From 2f4b198ad36082700c24f5f2fe81663a603675a0 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 8 Nov 2024 06:55:43 +0000 Subject: [PATCH] [V15] Updated dotnet template for Umbraco Packages with Bellisima (#17108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [WIP] Create Umbraco/Bellissima Package * Removes existing 'UmbracoPackage' This is because the RCL based one will be the only one going forward * Rename existing UmbracoPackageRCL to UmbracoPackage * Drop the mentions of RCL in the identifiers * CodeQL GitHub Action is complaining due to V15 wanting v9 .NET * Rename UmbracoPackage template to UmbracoExtension As this will only scaffold an extension and not other bits for a package such as Nuget, Github Actions & other things needed to be done to ship out a package * Remove package lock as npm install by the OS should generate this and can differ between Windows, Linux/OSX * Move JS clientside stuff into a folder called Client Will allow us to ignore the folder if or when doing a dotnet pack with a rule in CSProj * Add in .VSCode recommened extensions file to get the useful Lit Extension for completions in VSCode * For now remove the example dashboard & prop editor * Add a simple entrypoint * Fix path for primary output after rename * Use link suggested from Lotte * Use backofficeEntryPoint as entryPoint is deprecated * Update the umbraco-package.json to opt into telemetry as per PR suggestion * Improve commented code to include a link to docs * Improve readme from suggestions * Updates package.json to use latest Vite & TS Copies the tsconfig from the default scaffolding of vite lit-ts CLI * Adds the base property suggestion from Jacob & puts in a comment as to what its used for * Work in progress from hackathon day/afternoon * Hey-API generating a HTTP Client had changed and was a PITA to figure out what had changed Things to do for next time: Include these files if they include --include-samples flag * constants.ts * Controllers/ * Composers/ * client/src/api * client/src/dashboards/ * client/src/entrypoints Change file contents * client/src/bundle.manifests.ts * client/src/package.json (extra dependencies) * Adds in new property/flag/switch for dotnet new template * Warren cant spell Whether 🙈 * Update template.json to exclude the sample files if flag is not set/false * Make SLN happy/build * Conditional content in files for IncludeExample flag/switch * Need to include the content otherwise it doesnt get packed by nuget * Fix the path for the openapi-ts-config.ts file to be included/excluded * Use the project name from the dotnet new to help name manifests * Update namespaces so they get updated when dotnet new templatge is run with the --name * Updated example * Fix up VS Code recommended extension for Lit VSCode Should be .json not a .ts * Fix up build - as we dont use the imported UmbCurrentUserContext * Remove the relative path to the JSON schema as unable to know path to the web project * Typo fix as spooted by Rich * Update templates/UmbracoExtension/.template.config/template.json Co-authored-by: Lotte Pitcher * Adds a --site-domain flag/switch to use for setting the domain prefix Sets a default value of https://localhost:5000 We have no way of knowing what URL/domain the site is running at for the Umbraco website project * Rename stuff so its not 'example' & only have ping if include-examples is not set * As agreed with Lotte makes sense we always generate OpenAPI & TS client * Update umbraco-extension description * Generic node script to generate the TS client Checks if it can connect to it first and prompts user to ensure Umbraco site running or perhaps they need to change the URL in package.json for the node script * Generated API has conditional stuff in now to have just Ping or the more examples based on switch/flag * Adds symbols safeNamespace and safeName safeNamespace uses the one built in and then safeName, depends on the cleaned namespace to then use a custom transform (forms) to then use a regex replace on . and _ to ensure we have a nicer name still for namespaces, class names, URL/routes for the swagger etc... * change to use Umbraco.Extension as sourcename - check \.template.config\readme.md for 'placeholder' guidance * use '-sd' as shortname for site-domain as otherwise shows up as '-p:d' * fix typescript build error when not including examples * use provided name for API description as always being added * Missing renames of Contrioller stuff with Lotte @ hackathon * We missed the ctor * Titlecase the API URLs for Swagger/API Controller * dashboard tweaks * Missing [Server] on Whats the Time Modal/UUI-box --------- Co-authored-by: leekelleher Co-authored-by: Lotte Pitcher Co-authored-by: Lotte Pitcher --- templates/Umbraco.Templates.csproj | 12 +- .../.template.config/dotnetcli.host.json | 8 + .../.template.config/ide.host.json | 35 ++ .../.template.config/readme.md | 25 ++ .../.template.config/template.json | 170 ++++++++ .../Client/.vscode/extensions.json | 5 + .../UmbracoExtension/Client/package.json | 21 + .../Client/public/umbraco-package.json | 14 + .../Client/scripts/generate-openapi.js | 48 +++ .../UmbracoExtension/Client/src/api/index.ts | 6 + .../Client/src/api/schemas.gen.ts | 391 ++++++++++++++++++ .../Client/src/api/services.gen.ts | 41 ++ .../Client/src/api/types.gen.ts | 107 +++++ .../Client/src/bundle.manifests.ts | 13 + .../src/dashboards/dashboard.element.ts | 176 ++++++++ .../Client/src/dashboards/manifest.ts | 18 + .../Client/src/entrypoints/entrypoint.ts | 38 ++ .../Client/src/entrypoints/manifest.ts | 8 + .../UmbracoExtension/Client/tsconfig.json | 26 ++ .../UmbracoExtension/Client/vite.config.ts | 17 + .../Composers/UmbracoExtensionApiComposer.cs | 73 ++++ templates/UmbracoExtension/Constants.cs | 7 + .../UmbracoExtensionApiController.cs | 49 +++ .../UmbracoExtensionApiControllerBase.cs | 16 + templates/UmbracoExtension/README.txt | 38 ++ .../Umbraco.Extension.csproj} | 23 +- .../.template.config/dotnetcli.host.json | 18 - .../.template.config/ide.host.json | 15 - .../.template.config/template.json | 108 ----- .../UmbracoPackage/umbraco-package.json | 6 - .../UmbracoPackage/UmbracoPackage.csproj | 27 -- .../buildTransitive/UmbracoPackage.targets | 21 - .../.template.config/ide.host.json | 20 - .../.template.config/template.json | 82 ---- .../wwwroot/umbraco-package.json | 6 - 35 files changed, 1375 insertions(+), 313 deletions(-) rename templates/{UmbracoPackageRcl => UmbracoExtension}/.template.config/dotnetcli.host.json (72%) create mode 100644 templates/UmbracoExtension/.template.config/ide.host.json create mode 100644 templates/UmbracoExtension/.template.config/readme.md create mode 100644 templates/UmbracoExtension/.template.config/template.json create mode 100644 templates/UmbracoExtension/Client/.vscode/extensions.json create mode 100644 templates/UmbracoExtension/Client/package.json create mode 100644 templates/UmbracoExtension/Client/public/umbraco-package.json create mode 100644 templates/UmbracoExtension/Client/scripts/generate-openapi.js create mode 100644 templates/UmbracoExtension/Client/src/api/index.ts create mode 100644 templates/UmbracoExtension/Client/src/api/schemas.gen.ts create mode 100644 templates/UmbracoExtension/Client/src/api/services.gen.ts create mode 100644 templates/UmbracoExtension/Client/src/api/types.gen.ts create mode 100644 templates/UmbracoExtension/Client/src/bundle.manifests.ts create mode 100644 templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts create mode 100644 templates/UmbracoExtension/Client/src/dashboards/manifest.ts create mode 100644 templates/UmbracoExtension/Client/src/entrypoints/entrypoint.ts create mode 100644 templates/UmbracoExtension/Client/src/entrypoints/manifest.ts create mode 100644 templates/UmbracoExtension/Client/tsconfig.json create mode 100644 templates/UmbracoExtension/Client/vite.config.ts create mode 100644 templates/UmbracoExtension/Composers/UmbracoExtensionApiComposer.cs create mode 100644 templates/UmbracoExtension/Constants.cs create mode 100644 templates/UmbracoExtension/Controllers/UmbracoExtensionApiController.cs create mode 100644 templates/UmbracoExtension/Controllers/UmbracoExtensionApiControllerBase.cs create mode 100644 templates/UmbracoExtension/README.txt rename templates/{UmbracoPackageRcl/UmbracoPackage.csproj => UmbracoExtension/Umbraco.Extension.csproj} (50%) delete mode 100644 templates/UmbracoPackage/.template.config/dotnetcli.host.json delete mode 100644 templates/UmbracoPackage/.template.config/ide.host.json delete mode 100644 templates/UmbracoPackage/.template.config/template.json delete mode 100644 templates/UmbracoPackage/App_Plugins/UmbracoPackage/umbraco-package.json delete mode 100644 templates/UmbracoPackage/UmbracoPackage.csproj delete mode 100644 templates/UmbracoPackage/buildTransitive/UmbracoPackage.targets delete mode 100644 templates/UmbracoPackageRcl/.template.config/ide.host.json delete mode 100644 templates/UmbracoPackageRcl/.template.config/template.json delete mode 100644 templates/UmbracoPackageRcl/wwwroot/umbraco-package.json diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index b75df9f9a5..44d073a109 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -16,10 +16,9 @@ UmbracoProject\Program.cs UmbracoProject - - - + + UmbracoProject\Views\Partials\blocklist\%(RecursiveDir)%(Filename)%(Extension) UmbracoProject\Views\Partials\blocklist @@ -40,6 +39,13 @@ + + + + + + + <_TemplateJsonFiles Include="**\.template.config\template.json" Exclude="bin\**;obj\**" /> diff --git a/templates/UmbracoPackageRcl/.template.config/dotnetcli.host.json b/templates/UmbracoExtension/.template.config/dotnetcli.host.json similarity index 72% rename from templates/UmbracoPackageRcl/.template.config/dotnetcli.host.json rename to templates/UmbracoExtension/.template.config/dotnetcli.host.json index 9a960b348e..a3eea8aab7 100644 --- a/templates/UmbracoPackageRcl/.template.config/dotnetcli.host.json +++ b/templates/UmbracoExtension/.template.config/dotnetcli.host.json @@ -17,6 +17,14 @@ "SupportPagesAndViews": { "longName": "support-pages-and-views", "shortName": "s" + }, + "IncludeExample": { + "longName": "include-example", + "shortName": "ex" + }, + "SiteDomain": { + "longName": "site-domain", + "shortName": "sd" } } } diff --git a/templates/UmbracoExtension/.template.config/ide.host.json b/templates/UmbracoExtension/.template.config/ide.host.json new file mode 100644 index 0000000000..324a4045ae --- /dev/null +++ b/templates/UmbracoExtension/.template.config/ide.host.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json.schemastore.org/ide.host.json", + "order": 0, + "icon": "../../icon.png", + "description": { + "id": "umbraco-extension", + "text": "Umbraco Extension - A Razor Class Library project for building Umbraco extensions." + }, + "symbolInfo": [ + { + "id": "UmbracoVersion", + "isVisible": true + }, + { + "id": "SupportPagesAndViews", + "isVisible": true, + "persistenceScope": "templateGroup" + }, + { + "id": "IncludeExample", + "isVisible": true, + "description": { + "text": "Whether to include an example dashboard and supporting code" + } + }, + { + "id": "SiteDomain", + "isVisible": true, + "defaultValue": "https://localhost:5000", + "description": { + "text": "If using the --include-example then you can supply the domain prefix such as 'https://localhost:5000' to communicate with the Umbraco website for generating the TypeScript OpenAPI client" + } + } + ] +} diff --git a/templates/UmbracoExtension/.template.config/readme.md b/templates/UmbracoExtension/.template.config/readme.md new file mode 100644 index 0000000000..3dea89fff0 --- /dev/null +++ b/templates/UmbracoExtension/.template.config/readme.md @@ -0,0 +1,25 @@ +# Customising the umbraco-extension template + +## Source Name + +The source name is set to `Umbraco.Extension` + +The templating engine will rename any folder or file whose name contains `Umbraco.Extension` replacing it with the provided name. + +The templating engine will replace the text in any file as follows: + +- `Umbraco.Extension` with the safe namespace for the provided name +- `Umbraco_Extension` with the safe default class name for the provided name +- `umbraco.extension` with the safe namespace for the provided name, in lower case +- `umbraco_extension` with the safe default class name for the provided name, in lower case + +## Custom Replacements + +The following custom placeholders have been configured in `template.json`: + +- `UmbracoExtension` will be replaced with the safe namespace but without . or _ +- `umbracoextension` will be replaced with the safe namespace but without . or _ , in lower case +- `umbraco-extension` will be replaced with the kebab case transform of the provided name +- `Umbraco Extension` will be replaced with a 'friendly' version of the provided name, e.g. MyProject > My Project. NB it will render a trailing space so you don't need to add one. + +The first three custom placeholders have been configured to replace the text in both files and filenames. \ No newline at end of file diff --git a/templates/UmbracoExtension/.template.config/template.json b/templates/UmbracoExtension/.template.config/template.json new file mode 100644 index 0000000000..5ada4ae33f --- /dev/null +++ b/templates/UmbracoExtension/.template.config/template.json @@ -0,0 +1,170 @@ +{ + "$schema": "https://json.schemastore.org/template.json", + "author": "Umbraco HQ", + "classifications": [ + "Web", + "CMS", + "Umbraco", + "Extension", + "Plugin", + "Razor Class Library" + ], + "name": "Umbraco Extension", + "description": "A Razor Class Library project for building Umbraco extensions.", + "groupIdentity": "Umbraco.Templates.UmbracoExtension", + "identity": "Umbraco.Templates.UmbracoExtension", + "shortName": "umbraco-extension", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "Umbraco.Extension", + "defaultName": "Umbraco.Extension", + "preferNameDirectory": true, + "symbols": { + "Framework": { + "displayName": "Framework", + "description": "The target framework for the project.", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "displayName": ".NET 9.0", + "description": "Target net9.0", + "choice": "net9.0" + } + ], + "defaultValue": "net9.0", + "replaces": "net9.0" + }, + "UmbracoVersion": { + "displayName": "Umbraco version", + "description": "The version of Umbraco.Cms to add as PackageReference. By default it installs the latest non pre-release version", + "type": "parameter", + "datatype": "string", + "defaultValue": "*", + "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" + }, + "SkipRestore": { + "displayName": "Skip restore", + "description": "If specified, skips the automatic restore of the project on create.", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "SupportPagesAndViews": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Support pages and views", + "description": "Whether to support adding traditional Razor pages and Views to this library." + }, + "KebabCasedName": { + "type": "derived", + "valueSource": "name", + "replaces": "umbraco-extension", + "fileRename": "umbraco-extension", + "valueTransform": "kebabCase" + }, + "SafeName": { + "type": "derived", + "valueSource": "name", + "valueTransform": "safe_namespace" + }, + "SafeCleanName": { + "type": "derived", + "valueSource": "SafeName", + "replaces": "UmbracoExtension", + "fileRename": "UmbracoExtension", + "valueTransform": "removePunctuation" + }, + "SafeCleanNameLower": { + "type": "derived", + "valueSource": "SafeCleanName", + "replaces": "umbracoextension", + "fileRename": "umbracoextension", + "valueTransform": "lowerCase" + }, + "SafeCleanNameFriendly": { + "type": "derived", + "valueSource": "SafeCleanName", + "replaces": "Umbraco Extension", + "valueTransform": "pascalCaseToSpaces" + }, + "IncludeExample": { + "displayName": "Include Example Code", + "description": "Whether to include an example dashboard and other code to get started with.", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "SiteDomain": { + "displayName": "Site Domain", + "description": "If using the --include-example then you can supply the domain prefix such as 'https://localhost:5000' to communicate with the Umbraco website for generating the TypeScript OpenAPI client", + "type": "parameter", + "datatype": "string", + "defaultValue": "https://localhost:5000", + "replaces": "https://localhost:44339" + } + }, + "forms": { + "removePunctuation": { + "identifier": "replace", + "pattern": "[\\._]", + "replacement": "" + }, + "pascalCaseToSpaces": { + "identifier": "replace", + "pattern": "([A-Z][a-z]+)", + "replacement": "$1 " + } + }, + "primaryOutputs": [ + { + "path": "Umbraco.Extension.csproj" + } + ], + "postActions": [ + { + "id": "restore", + "condition": "(!SkipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + }, + { + "actionId": "3A7C4B45-1F5D-4A30-959A-51B88E82B5D2", + "args": { + "executable": "powershell", + "args": "cd Client;npm install;npm run build;", + "redirectStandardError": false, + "redirectStandardOutput": false + }, + "manualInstructions": [ + { + "text": "From the 'Client' folder run 'npm install' and then 'npm run build'" + } + ], + "continueOnError": true, + "description ": "Installs node modules" + } + ], + "sources": [ + { + "modifiers": [ + { + "condition": "(!IncludeExample)", + "exclude": [ + "[Cc]lient/src/dashboards/**", + "[Cc]lient/src/api/schemas.gen.ts" + ] + } + ] + } + ] +} diff --git a/templates/UmbracoExtension/Client/.vscode/extensions.json b/templates/UmbracoExtension/Client/.vscode/extensions.json new file mode 100644 index 0000000000..c152378477 --- /dev/null +++ b/templates/UmbracoExtension/Client/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "runem.lit-plugin" + ] +} diff --git a/templates/UmbracoExtension/Client/package.json b/templates/UmbracoExtension/Client/package.json new file mode 100644 index 0000000000..c4b1ae9b6b --- /dev/null +++ b/templates/UmbracoExtension/Client/package.json @@ -0,0 +1,21 @@ +{ + "name": "umbraco-extension", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "watch": "tsc && vite build --watch", + "build": "tsc && vite build", + "generate-client": "node scripts/generate-openapi.js https://localhost:44339/umbraco/swagger/umbracoextension/swagger.json" + }, + "devDependencies": { + "@hey-api/client-fetch": "^0.4.2", + "@hey-api/openapi-ts": "^0.53.11", + "@umbraco-cms/backoffice": "^UMBRACO_VERSION_FROM_TEMPLATE", + "chalk": "^5.3.0", + "cross-env": "^7.0.3", + "node-fetch": "^3.3.2", + "typescript": "^5.6.3", + "vite": "^5.4.9" + } +} diff --git a/templates/UmbracoExtension/Client/public/umbraco-package.json b/templates/UmbracoExtension/Client/public/umbraco-package.json new file mode 100644 index 0000000000..2ca2f83317 --- /dev/null +++ b/templates/UmbracoExtension/Client/public/umbraco-package.json @@ -0,0 +1,14 @@ +{ + "id": "Umbraco.Extension", + "name": "Umbraco.Extension", + "version": "0.0.0", + "allowPackageTelemetry": true, + "extensions": [ + { + "name": "Umbraco ExtensionBundle", + "alias": "Umbraco.Extension.Bundle", + "type": "bundle", + "js": "/App_Plugins/UmbracoExtension/umbraco-extension.js" + } + ] +} diff --git a/templates/UmbracoExtension/Client/scripts/generate-openapi.js b/templates/UmbracoExtension/Client/scripts/generate-openapi.js new file mode 100644 index 0000000000..93bb481289 --- /dev/null +++ b/templates/UmbracoExtension/Client/scripts/generate-openapi.js @@ -0,0 +1,48 @@ +import fetch from 'node-fetch'; +import chalk from 'chalk'; +import { createClient } from '@hey-api/openapi-ts'; + +// Start notifying user we are generating the TypeScript client +console.log(chalk.green("Generating OpenAPI client...")); + +const swaggerUrl = process.argv[2]; +if (swaggerUrl === undefined) { + console.error(chalk.red(`ERROR: Missing URL to OpenAPI spec`)); + console.error(`Please provide the URL to the OpenAPI spec as the first argument found in ${chalk.yellow('package.json')}`); + console.error(`Example: node generate-openapi.js ${chalk.yellow('https://localhost:44331/umbraco/swagger/REPLACE_ME/swagger.json')}`); + process.exit(); +} + +// Needed to ignore self-signed certificates from running Umbraco on https on localhost +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + +// Start checking to see if we can connect to the OpenAPI spec +console.log("Ensure your Umbraco instance is running"); +console.log(`Fetching OpenAPI definition from ${chalk.yellow(swaggerUrl)}`); + +fetch(swaggerUrl).then(response => { + if (!response.ok) { + console.error(chalk.red(`ERROR: OpenAPI spec returned with a non OK (200) response: ${response.status} ${response.statusText}`)); + console.error(`The URL to your Umbraco instance may be wrong or the instance is not running`); + console.error(`Please verify or change the URL in the ${chalk.yellow('package.json')} for the script ${chalk.yellow('generate-openapi')}`); + return; + } + + console.log(`OpenAPI spec fetched successfully`); + console.log(`Calling ${chalk.yellow('hey-api')} to generate TypeScript client`); + + createClient({ + client: '@hey-api/client-fetch', + input: swaggerUrl, + output: 'src/api', + services: { + asClass: true, + } + }); + +}) + .catch(error => { + console.error(`ERROR: Failed to connect to the OpenAPI spec: ${chalk.red(error.message)}`); + console.error(`The URL to your Umbraco instance may be wrong or the instance is not running`); + console.error(`Please verify or change the URL in the ${chalk.yellow('package.json')} for the script ${chalk.yellow('generate-openapi')}`); + }); diff --git a/templates/UmbracoExtension/Client/src/api/index.ts b/templates/UmbracoExtension/Client/src/api/index.ts new file mode 100644 index 0000000000..7661d65371 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/index.ts @@ -0,0 +1,6 @@ +// This file is auto-generated by @hey-api/openapi-ts +//#if(IncludeExample) +export * from './schemas.gen'; +//#endif +export * from './services.gen'; +export * from './types.gen'; diff --git a/templates/UmbracoExtension/Client/src/api/schemas.gen.ts b/templates/UmbracoExtension/Client/src/api/schemas.gen.ts new file mode 100644 index 0000000000..1a1885f5d5 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/schemas.gen.ts @@ -0,0 +1,391 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export const DocumentGranularPermissionModelSchema = { + required: ['context', 'key', 'permission'], + type: 'object', + properties: { + key: { + type: 'string', + format: 'uuid' + }, + context: { + type: 'string', + readOnly: true + }, + permission: { + type: 'string' + } + }, + additionalProperties: false +} as const; + +export const ReadOnlyUserGroupModelSchema = { + required: ['alias', 'allowedLanguages', 'allowedSections', 'granularPermissions', 'hasAccessToAllLanguages', 'id', 'key', 'name', 'permissions'], + type: 'object', + properties: { + id: { + type: 'integer', + format: 'int32' + }, + key: { + type: 'string', + format: 'uuid' + }, + name: { + type: 'string' + }, + icon: { + type: 'string', + nullable: true + }, + startContentId: { + type: 'integer', + format: 'int32', + nullable: true + }, + startMediaId: { + type: 'integer', + format: 'int32', + nullable: true + }, + alias: { + type: 'string' + }, + hasAccessToAllLanguages: { + type: 'boolean' + }, + allowedLanguages: { + type: 'array', + items: { + type: 'integer', + format: 'int32' + } + }, + permissions: { + uniqueItems: true, + type: 'array', + items: { + type: 'string' + } + }, + granularPermissions: { + uniqueItems: true, + type: 'array', + items: { + oneOf: [ + { + '$ref': '#/components/schemas/DocumentGranularPermissionModel' + }, + { + '$ref': '#/components/schemas/UnknownTypeGranularPermissionModel' + } + ] + } + }, + allowedSections: { + type: 'array', + items: { + type: 'string' + } + } + }, + additionalProperties: false +} as const; + +export const UnknownTypeGranularPermissionModelSchema = { + required: ['context', 'permission'], + type: 'object', + properties: { + context: { + type: 'string' + }, + permission: { + type: 'string' + } + }, + additionalProperties: false +} as const; + +export const UserGroupModelSchema = { + required: ['alias', 'allowedLanguages', 'allowedSections', 'createDate', 'granularPermissions', 'hasAccessToAllLanguages', 'hasIdentity', 'id', 'key', 'permissions', 'updateDate', 'userCount'], + type: 'object', + properties: { + id: { + type: 'integer', + format: 'int32' + }, + key: { + type: 'string', + format: 'uuid' + }, + createDate: { + type: 'string', + format: 'date-time' + }, + updateDate: { + type: 'string', + format: 'date-time' + }, + deleteDate: { + type: 'string', + format: 'date-time', + nullable: true + }, + hasIdentity: { + type: 'boolean', + readOnly: true + }, + startMediaId: { + type: 'integer', + format: 'int32', + nullable: true + }, + startContentId: { + type: 'integer', + format: 'int32', + nullable: true + }, + icon: { + type: 'string', + nullable: true + }, + alias: { + type: 'string' + }, + name: { + type: 'string', + nullable: true + }, + hasAccessToAllLanguages: { + type: 'boolean' + }, + permissions: { + uniqueItems: true, + type: 'array', + items: { + type: 'string' + } + }, + granularPermissions: { + uniqueItems: true, + type: 'array', + items: { + oneOf: [ + { + '$ref': '#/components/schemas/DocumentGranularPermissionModel' + }, + { + '$ref': '#/components/schemas/UnknownTypeGranularPermissionModel' + } + ] + } + }, + allowedSections: { + type: 'array', + items: { + type: 'string' + }, + readOnly: true + }, + userCount: { + type: 'integer', + format: 'int32', + readOnly: true + }, + allowedLanguages: { + type: 'array', + items: { + type: 'integer', + format: 'int32' + }, + readOnly: true + } + }, + additionalProperties: false +} as const; + +export const UserKindModelSchema = { + enum: ['Default', 'Api'], + type: 'string' +} as const; + +export const UserModelSchema = { + required: ['allowedSections', 'createDate', 'email', 'failedPasswordAttempts', 'groups', 'hasIdentity', 'id', 'isApproved', 'isLockedOut', 'key', 'kind', 'profileData', 'sessionTimeout', 'updateDate', 'username', 'userState'], + type: 'object', + properties: { + id: { + type: 'integer', + format: 'int32' + }, + key: { + type: 'string', + format: 'uuid' + }, + createDate: { + type: 'string', + format: 'date-time' + }, + updateDate: { + type: 'string', + format: 'date-time' + }, + deleteDate: { + type: 'string', + format: 'date-time', + nullable: true + }, + hasIdentity: { + type: 'boolean', + readOnly: true + }, + emailConfirmedDate: { + type: 'string', + format: 'date-time', + nullable: true + }, + invitedDate: { + type: 'string', + format: 'date-time', + nullable: true + }, + username: { + type: 'string' + }, + email: { + type: 'string' + }, + rawPasswordValue: { + type: 'string', + nullable: true + }, + passwordConfiguration: { + type: 'string', + nullable: true + }, + isApproved: { + type: 'boolean' + }, + isLockedOut: { + type: 'boolean' + }, + lastLoginDate: { + type: 'string', + format: 'date-time', + nullable: true + }, + lastPasswordChangeDate: { + type: 'string', + format: 'date-time', + nullable: true + }, + lastLockoutDate: { + type: 'string', + format: 'date-time', + nullable: true + }, + failedPasswordAttempts: { + type: 'integer', + format: 'int32' + }, + comments: { + type: 'string', + nullable: true + }, + userState: { + '$ref': '#/components/schemas/UserStateModel' + }, + name: { + type: 'string', + nullable: true + }, + allowedSections: { + type: 'array', + items: { + type: 'string' + }, + readOnly: true + }, + profileData: { + oneOf: [ + { + '$ref': '#/components/schemas/UserModel' + }, + { + '$ref': '#/components/schemas/UserProfileModel' + } + ], + readOnly: true + }, + securityStamp: { + type: 'string', + nullable: true + }, + avatar: { + type: 'string', + nullable: true + }, + sessionTimeout: { + type: 'integer', + format: 'int32' + }, + startContentIds: { + type: 'array', + items: { + type: 'integer', + format: 'int32' + }, + nullable: true + }, + startMediaIds: { + type: 'array', + items: { + type: 'integer', + format: 'int32' + }, + nullable: true + }, + language: { + type: 'string', + nullable: true + }, + kind: { + '$ref': '#/components/schemas/UserKindModel' + }, + groups: { + type: 'array', + items: { + oneOf: [ + { + '$ref': '#/components/schemas/ReadOnlyUserGroupModel' + }, + { + '$ref': '#/components/schemas/UserGroupModel' + } + ] + }, + readOnly: true + } + }, + additionalProperties: false +} as const; + +export const UserProfileModelSchema = { + required: ['id'], + type: 'object', + properties: { + id: { + type: 'integer', + format: 'int32' + }, + name: { + type: 'string', + nullable: true + } + }, + additionalProperties: false +} as const; + +export const UserStateModelSchema = { + enum: ['Active', 'Disabled', 'LockedOut', 'Invited', 'Inactive', 'All'], + type: 'string' +} as const; \ No newline at end of file diff --git a/templates/UmbracoExtension/Client/src/api/services.gen.ts b/templates/UmbracoExtension/Client/src/api/services.gen.ts new file mode 100644 index 0000000000..c9d1c0b91b --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/services.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createClient, createConfig, type Options } from '@hey-api/client-fetch'; +//#if(IncludeExample) +import type { PingError, PingResponse, WhatsMyNameError, WhatsMyNameResponse, WhatsTheTimeMrWolfError, WhatsTheTimeMrWolfResponse, WhoAmIError, WhoAmIResponse } from './types.gen'; +//#else +import type { PingError, PingResponse } from './types.gen'; +//#endif + +export const client = createClient(createConfig()); + +export class UmbracoExtensionService { + public static ping(options?: Options) { + return (options?.client ?? client).get({ + ...options, + url: '/umbraco/umbracoextension/api/v1/ping' + }); + } +//#if(IncludeExample) + public static whatsMyName(options?: Options) { + return (options?.client ?? client).get({ + ...options, + url: '/umbraco/umbracoextension/api/v1/whatsMyName' + }); + } + + public static whatsTheTimeMrWolf(options?: Options) { + return (options?.client ?? client).get({ + ...options, + url: '/umbraco/umbracoextension/api/v1/whatsTheTimeMrWolf' + }); + } + + public static whoAmI(options?: Options) { + return (options?.client ?? client).get({ + ...options, + url: '/umbraco/umbracoextension/api/v1/whoAmI' + }); + } +//#endif +} diff --git a/templates/UmbracoExtension/Client/src/api/types.gen.ts b/templates/UmbracoExtension/Client/src/api/types.gen.ts new file mode 100644 index 0000000000..e666792803 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/api/types.gen.ts @@ -0,0 +1,107 @@ +// This file is auto-generated by @hey-api/openapi-ts +//#if(IncludeExample) +export type DocumentGranularPermissionModel = { + key: string; + readonly context: string; + permission: string; +}; + +export type ReadOnlyUserGroupModel = { + id: number; + key: string; + name: string; + icon?: (string) | null; + startContentId?: (number) | null; + startMediaId?: (number) | null; + alias: string; + hasAccessToAllLanguages: boolean; + allowedLanguages: Array<(number)>; + permissions: Array<(string)>; + granularPermissions: Array<(DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel)>; + allowedSections: Array<(string)>; +}; + +export type UnknownTypeGranularPermissionModel = { + context: string; + permission: string; +}; + +export type UserGroupModel = { + id: number; + key: string; + createDate: string; + updateDate: string; + deleteDate?: (string) | null; + readonly hasIdentity: boolean; + startMediaId?: (number) | null; + startContentId?: (number) | null; + icon?: (string) | null; + alias: string; + name?: (string) | null; + hasAccessToAllLanguages: boolean; + permissions: Array<(string)>; + granularPermissions: Array<(DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel)>; + readonly allowedSections: Array<(string)>; + readonly userCount: number; + readonly allowedLanguages: Array<(number)>; +}; + +export type UserKindModel = 'Default' | 'Api'; + +export type UserModel = { + id: number; + key: string; + createDate: string; + updateDate: string; + deleteDate?: (string) | null; + readonly hasIdentity: boolean; + emailConfirmedDate?: (string) | null; + invitedDate?: (string) | null; + username: string; + email: string; + rawPasswordValue?: (string) | null; + passwordConfiguration?: (string) | null; + isApproved: boolean; + isLockedOut: boolean; + lastLoginDate?: (string) | null; + lastPasswordChangeDate?: (string) | null; + lastLockoutDate?: (string) | null; + failedPasswordAttempts: number; + comments?: (string) | null; + userState: UserStateModel; + name?: (string) | null; + readonly allowedSections: Array<(string)>; + readonly profileData: (UserModel | UserProfileModel); + securityStamp?: (string) | null; + avatar?: (string) | null; + sessionTimeout: number; + startContentIds?: Array<(number)> | null; + startMediaIds?: Array<(number)> | null; + language?: (string) | null; + kind: UserKindModel; + readonly groups: Array<(ReadOnlyUserGroupModel | UserGroupModel)>; +}; + +export type UserProfileModel = { + id: number; + name?: (string) | null; +}; + +export type UserStateModel = 'Active' | 'Disabled' | 'LockedOut' | 'Invited' | 'Inactive' | 'All'; +//#endif +export type PingResponse = (string); + +export type PingError = (unknown); +//#if(IncludeExample) +export type WhatsMyNameResponse = (string); + +export type WhatsMyNameError = (unknown); + +export type WhatsTheTimeMrWolfResponse = (string); + +export type WhatsTheTimeMrWolfError = (unknown); + +export type WhoAmIResponse = ((UserModel)); + +export type WhoAmIError = (unknown); +//#endif diff --git a/templates/UmbracoExtension/Client/src/bundle.manifests.ts b/templates/UmbracoExtension/Client/src/bundle.manifests.ts new file mode 100644 index 0000000000..fb2d2bfa09 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/bundle.manifests.ts @@ -0,0 +1,13 @@ +import { manifests as entrypoints } from './entrypoints/manifest'; +//#if IncludeExample +import { manifests as dashboards } from './dashboards/manifest'; +//#endif + +// Job of the bundle is to collate all the manifests from different parts of the extension and load other manifests +// We load this bundle from umbraco-package.json +export const manifests: Array = [ + ...entrypoints, + //#if IncludeExample + ...dashboards, + //#endif +]; diff --git a/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts b/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts new file mode 100644 index 0000000000..287464086e --- /dev/null +++ b/templates/UmbracoExtension/Client/src/dashboards/dashboard.element.ts @@ -0,0 +1,176 @@ +import { LitElement, css, html, customElement, state } from "@umbraco-cms/backoffice/external/lit"; +import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api"; +import { UmbracoExtensionService, UserModel } from "../api"; +import { UUIButtonElement } from "@umbraco-cms/backoffice/external/uui"; +import { UMB_NOTIFICATION_CONTEXT, UmbNotificationContext } from "@umbraco-cms/backoffice/notification"; +import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from "@umbraco-cms/backoffice/current-user"; + +@customElement('example-dashboard') +export class ExampleDashboardElement extends UmbElementMixin(LitElement) { + + @state() + private _yourName: string | undefined = "Press the button!"; + + @state() + private _timeFromMrWolf: Date | undefined; + + @state() + private _serverUserData: UserModel | undefined = undefined; + + @state() + private _contextCurrentUser: UmbCurrentUserModel | undefined = undefined; + + constructor() { + super(); + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => { + this.#notificationContext = notificationContext; + }); + + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (currentUserContext) => { + + // When we have the current user context + // We can observe properties from it, such as the current user or perhaps just individual properties + // When the currentUser object changes we will get notified and can reset the @state properrty + this.observe(currentUserContext.currentUser, (currentUser) => { + this._contextCurrentUser = currentUser; + }); + }); + } + + #notificationContext: UmbNotificationContext | undefined = undefined; + + #onClickWhoAmI = async (ev: Event) => { + const buttonElement = ev.target as UUIButtonElement; + buttonElement.state = "waiting"; + + const { data, error } = await UmbracoExtensionService.whoAmI(); + + if (error) { + buttonElement.state = "failed"; + console.error(error); + return; + } + + if (data !== undefined) { + this._serverUserData = data; + buttonElement.state = "success"; + } + + if (this.#notificationContext) { + this.#notificationContext.peek("warning", { + data: { + headline: `You are ${this._serverUserData?.name}`, + message: `Your email is ${this._serverUserData?.email}`, + } + }) + } + } + + #onClickWhatsTheTimeMrWolf = async (ev: Event) => { + const buttonElement = ev.target as UUIButtonElement; + buttonElement.state = "waiting"; + + // Getting a string - should I expect a datetime?! + const { data, error } = await UmbracoExtensionService.whatsTheTimeMrWolf(); + + if (error) { + buttonElement.state = "failed"; + console.error(error); + return; + } + + if (data !== undefined) { + this._timeFromMrWolf = new Date(data); + buttonElement.state = "success"; + } + } + + #onClickWhatsMyName = async (ev: Event) => { + const buttonElement = ev.target as UUIButtonElement; + buttonElement.state = "waiting"; + + const { data, error } = await UmbracoExtensionService.whatsMyName(); + + if (error) { + buttonElement.state = "failed"; + console.error(error); + return; + } + + this._yourName = data; + buttonElement.state = "success"; + } + + render() { + return html` + +
[Server]
+

${this._serverUserData?.email ? this._serverUserData.email : 'Press the button!'}

+
    + ${this._serverUserData?.groups.map(group => html`
  • ${group.name}
  • `)} +
+ + Who am I? + +

This endpoint gets your current user from the server and displays your email and list of user groups. + It also displays a Notification with your details.

+
+ + +
[Server]
+

${this._yourName }

+ + Whats my name? + +

This endpoint has a forced delay to show the button 'waiting' state for a few seconds before completing the request.

+
+ + +
[Server]
+

${this._timeFromMrWolf ? this._timeFromMrWolf.toLocaleString() : 'Press the button!'}

+ + Whats the time Mr Wolf? + +

This endpoint gets the current date and time from the server.

+
+ + +
[Context]
+

Current user email: ${this._contextCurrentUser?.email}

+

This is the JSON object available by consuming the 'UMB_CURRENT_USER_CONTEXT' context:

+ ${JSON.stringify(this._contextCurrentUser, null, 2)} +
+ `; + } + + static styles = [ + css` + :host { + display: grid; + gap: var(--uui-size-layout-1); + padding: var(--uui-size-layout-1); + grid-template-columns: 1fr 1fr 1fr; + } + + uui-box { + margin-bottom: var(--uui-size-layout-1); + } + + h2 { + margin-top:0; + } + + .wide { + grid-column: span 3; + } + `]; +} + +export default ExampleDashboardElement; + +declare global { + interface HTMLElementTagNameMap { + 'example-dashboard': ExampleDashboardElement; + } +} diff --git a/templates/UmbracoExtension/Client/src/dashboards/manifest.ts b/templates/UmbracoExtension/Client/src/dashboards/manifest.ts new file mode 100644 index 0000000000..a020e3ab2c --- /dev/null +++ b/templates/UmbracoExtension/Client/src/dashboards/manifest.ts @@ -0,0 +1,18 @@ +export const manifests: Array = [ + { + name: "Umbraco ExtensionDashboard", + alias: "Umbraco.Extension.Dashboard", + type: 'dashboard', + js: () => import("./dashboard.element"), + meta: { + label: "Example Dashboard", + pathname: "example-dashboard" + }, + conditions: [ + { + alias: 'Umb.Condition.SectionAlias', + match: 'Umb.Section.Content', + } + ], + } +]; diff --git a/templates/UmbracoExtension/Client/src/entrypoints/entrypoint.ts b/templates/UmbracoExtension/Client/src/entrypoints/entrypoint.ts new file mode 100644 index 0000000000..65796c3202 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/entrypoints/entrypoint.ts @@ -0,0 +1,38 @@ +import { UmbEntryPointOnInit, UmbEntryPointOnUnload } from '@umbraco-cms/backoffice/extension-api'; +//#if IncludeExample +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { client } from '../api'; +//#endif + +// load up the manifests here +export const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => { + + console.log('Hello from my extension 🎉'); + //#if IncludeExample + // Will use only to add in Open API config with generated TS OpenAPI HTTPS Client + // Do the OAuth token handshake stuff + _host.consumeContext(UMB_AUTH_CONTEXT, async (authContext) => { + + // Get the token info from Umbraco + const config = authContext.getOpenApiConfiguration(); + + client.setConfig({ + baseUrl: config.base, + credentials: config.credentials + }); + + // For every request being made, add the token to the headers + // Can't use the setConfig approach above as its set only once and + // tokens expire and get refreshed + client.interceptors.request.use(async (request, _options) => { + const token = await config.token(); + request.headers.set('Authorization', `Bearer ${token}`); + return request; + }); + }); + //#endif +}; + +export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => { + console.log('Goodbye from my extension 👋'); +}; diff --git a/templates/UmbracoExtension/Client/src/entrypoints/manifest.ts b/templates/UmbracoExtension/Client/src/entrypoints/manifest.ts new file mode 100644 index 0000000000..5dcda9de89 --- /dev/null +++ b/templates/UmbracoExtension/Client/src/entrypoints/manifest.ts @@ -0,0 +1,8 @@ +export const manifests: Array = [ + { + name: "Umbraco ExtensionEntrypoint", + alias: "Umbraco.Extension.Entrypoint", + type: "backofficeEntryPoint", + js: () => import("./entrypoint"), + } +]; diff --git a/templates/UmbracoExtension/Client/tsconfig.json b/templates/UmbracoExtension/Client/tsconfig.json new file mode 100644 index 0000000000..1b3364784e --- /dev/null +++ b/templates/UmbracoExtension/Client/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ "ES2020", "DOM", "DOM.Iterable" ], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "types": [ "@umbraco-cms/backoffice/extension-types" ] + }, + "include": [ "src" ] +} diff --git a/templates/UmbracoExtension/Client/vite.config.ts b/templates/UmbracoExtension/Client/vite.config.ts new file mode 100644 index 0000000000..5232076b8c --- /dev/null +++ b/templates/UmbracoExtension/Client/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + lib: { + entry: "src/bundle.manifests.ts", // Bundle registers one or more manifests + formats: ["es"], + fileName: "umbraco-extension", + }, + outDir: "../wwwroot/App_Plugins/UmbracoExtension", // your web component will be saved in this location + emptyOutDir: true, + sourcemap: true, + rollupOptions: { + external: [/^@umbraco/], + }, + } +}); diff --git a/templates/UmbracoExtension/Composers/UmbracoExtensionApiComposer.cs b/templates/UmbracoExtension/Composers/UmbracoExtensionApiComposer.cs new file mode 100644 index 0000000000..481e1d19bb --- /dev/null +++ b/templates/UmbracoExtension/Composers/UmbracoExtensionApiComposer.cs @@ -0,0 +1,73 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Api.Management.OpenApi; +using Umbraco.Cms.Api.Common.OpenApi; + +namespace Umbraco.Extension.Composers +{ + public class UmbracoExtensionApiComposer : IComposer + { + public void Compose(IUmbracoBuilder builder) + { + + builder.Services.AddSingleton(); + + builder.Services.Configure(opt => + { + // Related documentation: + // https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api + // https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/adding-a-custom-swagger-document + // https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/versioning-your-api + // https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/access-policies + + // Configure the Swagger generation options + // Add in a new Swagger API document solely for our own package that can be browsed via Swagger UI + // Along with having a generated swagger JSON file that we can use to auto generate a TypeScript client + opt.SwaggerDoc(Constants.ApiName, new OpenApiInfo + { + Title = "Umbraco ExtensionBackoffice API", + Version = "1.0", + // Contact = new OpenApiContact + // { + // Name = "Some Developer", + // Email = "you@company.com", + // Url = new Uri("https://company.com") + // } + }); + + // Enable Umbraco authentication for the "Example" Swagger document + // PR: https://github.com/umbraco/Umbraco-CMS/pull/15699 + opt.OperationFilter(); + }); + } + + public class UmbracoExtensionOperationSecurityFilter : BackOfficeSecurityRequirementsOperationFilterBase + { + protected override string ApiName => Constants.ApiName; + } + + // This is used to generate nice operation IDs in our swagger json file + // So that the gnerated TypeScript client has nice method names and not too verbose + // https://docs.umbraco.com/umbraco-cms/tutorials/creating-a-backoffice-api/umbraco-schema-and-operation-ids#operation-ids + public class CustomOperationHandler : OperationIdHandler + { + public CustomOperationHandler(IOptions apiVersioningOptions) : base(apiVersioningOptions) + { + } + + protected override bool CanHandle(ApiDescription apiDescription, ControllerActionDescriptor controllerActionDescriptor) + { + return controllerActionDescriptor.ControllerTypeInfo.Namespace?.StartsWith("Umbraco.Extension.Controllers", comparisonType: StringComparison.InvariantCultureIgnoreCase) is true; + } + + public override string Handle(ApiDescription apiDescription) => $"{apiDescription.ActionDescriptor.RouteValues["action"]}"; + } + } +} diff --git a/templates/UmbracoExtension/Constants.cs b/templates/UmbracoExtension/Constants.cs new file mode 100644 index 0000000000..9fc6796da1 --- /dev/null +++ b/templates/UmbracoExtension/Constants.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Extension +{ + public class Constants + { + public const string ApiName = "umbracoextension"; + } +} diff --git a/templates/UmbracoExtension/Controllers/UmbracoExtensionApiController.cs b/templates/UmbracoExtension/Controllers/UmbracoExtensionApiController.cs new file mode 100644 index 0000000000..2f62ec2785 --- /dev/null +++ b/templates/UmbracoExtension/Controllers/UmbracoExtensionApiController.cs @@ -0,0 +1,49 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +#if IncludeExample +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; +#endif + +namespace Umbraco.Extension.Controllers +{ + [ApiVersion("1.0")] + [ApiExplorerSettings(GroupName = "Umbraco.Extension")] + public class UmbracoExtensionApiController : UmbracoExtensionApiControllerBase + { +#if IncludeExample + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public UmbracoExtensionApiController(IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } +#endif + + [HttpGet("ping")] + [ProducesResponseType(StatusCodes.Status200OK)] + public string Ping() => "Pong"; +#if IncludeExample + + [HttpGet("whatsTheTimeMrWolf")] + [ProducesResponseType(typeof(DateTime), 200)] + public DateTime WhatsTheTimeMrWolf() => DateTime.Now; + + [HttpGet("whatsMyName")] + [ProducesResponseType(StatusCodes.Status200OK)] + public string WhatsMyName() + { + // So we can see a long request in the dashboard with a spinning progress wheel + Thread.Sleep(2000); + + var currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + return currentUser?.Name ?? "I have no idea who you are"; + } + + [HttpGet("whoAmI")] + [ProducesResponseType(StatusCodes.Status200OK)] + public IUser? WhoAmI() => _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; +#endif + } +} diff --git a/templates/UmbracoExtension/Controllers/UmbracoExtensionApiControllerBase.cs b/templates/UmbracoExtension/Controllers/UmbracoExtensionApiControllerBase.cs new file mode 100644 index 0000000000..3e5a5e14a0 --- /dev/null +++ b/templates/UmbracoExtension/Controllers/UmbracoExtensionApiControllerBase.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.Routing; + +namespace Umbraco.Extension.Controllers +{ + [ApiController] + [BackOfficeRoute("umbracoextension/api/v{version:apiVersion}")] + [Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] + [MapToApi(Constants.ApiName)] + public class UmbracoExtensionApiControllerBase : ControllerBase + { + } +} diff --git a/templates/UmbracoExtension/README.txt b/templates/UmbracoExtension/README.txt new file mode 100644 index 0000000000..7beee9137e --- /dev/null +++ b/templates/UmbracoExtension/README.txt @@ -0,0 +1,38 @@ + _ _ _ + | | | | | | + __| | ___ | |_ _ __ ___| |_ _ __ _____ __ + / _` |/ _ \| __| '_ \ / _ \ __| | '_ \ / _ \ \ /\ / / + | (_| | (_) | |_| | | | __/ |_ | | | | __/\ V V / + \__,_|\___/ \__|_| |_|\___|\__| |_| |_|\___| \_/\_/ _ _ + | | | | (_) + _ _ _ __ ___ | |__ _ __ __ _ ___ ___ _____ _| |_ ___ _ __ ___ _ ___ _ __ + | | | | '_ ` _ \| '_ \| '__/ _` |/ __/ _ \ / _ \ \/ / __/ _ \ '_ \/ __| |/ _ \| '_ \ + | |_| | | | | | | |_) | | | (_| | (_| (_) | | __/> <| || __/ | | \__ \ | (_) | | | | + \__,_|_| |_| |_|_.__/|_| \__,_|\___\___/ \___/_/\_\\__\___|_| |_|___/_|\___/|_| |_| + + +== Requirements == +* Node LTS Version 20.17.0+ +* Use a tool such as NVM (Node Version Manager) for your OS to help manage multiple versions of Node + +== Node Version Manager tools == +* https://github.com/coreybutler/nvm-windows +* https://github.com/nvm-sh/nvm +* https://docs.volta.sh/guide/getting-started + +== Steps == +* Open a terminal inside the `\Client` folder +* Run `npm install` to install all the dependencies +* Run `npm run build` to build the project +* The build output is copied to `wwwroot\App_Plugins\UmbracoExtension\umbraco-extension.js` + +== File Watching == +* Add this Razor Class Library Project as a project reference to an Umbraco Website project +* From the `\Client` folder run the command `npm run watch` this will monitor the changes to the *.ts files and rebuild the project +* With the Umbraco website project running the Razor Class Library Project will refresh the browser when the build is complete + +== Suggestion == +* Use VSCode as the editor of choice as it has good tooling support for TypeScript and it will recommend a VSCode Extension for good Lit WebComponent completions + +== Other Resources == +* Umbraco Docs - https://docs.umbraco.com/umbraco-cms/customizing/extend-and-customize-editing-experience diff --git a/templates/UmbracoPackageRcl/UmbracoPackage.csproj b/templates/UmbracoExtension/Umbraco.Extension.csproj similarity index 50% rename from templates/UmbracoPackageRcl/UmbracoPackage.csproj rename to templates/UmbracoExtension/Umbraco.Extension.csproj index b959751c87..3b70140f35 100644 --- a/templates/UmbracoPackageRcl/UmbracoPackage.csproj +++ b/templates/UmbracoExtension/Umbraco.Extension.csproj @@ -4,16 +4,14 @@ enable enable true - UmbracoPackage - App_Plugins/UmbracoPackage + Umbraco.Extension + / - UmbracoPackage - UmbracoPackage - UmbracoPackage - ... - umbraco plugin package + Umbraco.Extension + Umbraco.Extension + Umbraco.Extension @@ -23,5 +21,16 @@ + + + + + + + + + + + diff --git a/templates/UmbracoPackage/.template.config/dotnetcli.host.json b/templates/UmbracoPackage/.template.config/dotnetcli.host.json deleted file mode 100644 index 6473c5c643..0000000000 --- a/templates/UmbracoPackage/.template.config/dotnetcli.host.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/dotnetcli.host.json", - "symbolInfo": { - "Framework": { - "longName": "Framework", - "shortName": "F", - "isHidden": true - }, - "UmbracoVersion": { - "longName": "version", - "shortName": "v" - }, - "SkipRestore": { - "longName": "no-restore", - "shortName": "" - } - } -} diff --git a/templates/UmbracoPackage/.template.config/ide.host.json b/templates/UmbracoPackage/.template.config/ide.host.json deleted file mode 100644 index 0464cfeb1f..0000000000 --- a/templates/UmbracoPackage/.template.config/ide.host.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/ide.host.json", - "order": 0, - "icon": "../../icon.png", - "description": { - "id": "UmbracoPackage", - "text": "Umbraco Package - An empty Umbraco CMS package/plugin." - }, - "symbolInfo": [ - { - "id": "UmbracoVersion", - "isVisible": true - } - ] -} diff --git a/templates/UmbracoPackage/.template.config/template.json b/templates/UmbracoPackage/.template.config/template.json deleted file mode 100644 index 5c93b1d68d..0000000000 --- a/templates/UmbracoPackage/.template.config/template.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/template.json", - "author": "Umbraco HQ", - "classifications": [ - "Web", - "CMS", - "Umbraco", - "Package", - "Plugin" - ], - "name": "Umbraco Package", - "description": "An empty Umbraco package/plugin project ready to get started.", - "groupIdentity": "Umbraco.Templates.UmbracoPackage", - "identity": "Umbraco.Templates.UmbracoPackage.CSharp", - "shortName": "umbracopackage", - "tags": { - "language": "C#", - "type": "project" - }, - "sourceName": "UmbracoPackage", - "defaultName": "UmbracoPackage1", - "preferNameDirectory": true, - "symbols": { - "Framework": { - "displayName": "Framework", - "description": "The target framework for the project.", - "type": "parameter", - "datatype": "choice", - "choices": [ - { - "displayName": ".NET 9.0", - "description": "Target net9.0", - "choice": "net9.0" - } - ], - "defaultValue": "net9.0", - "replaces": "net9.0" - }, - "UmbracoVersion": { - "displayName": "Umbraco version", - "description": "The version of Umbraco.Cms to add as PackageReference.", - "type": "parameter", - "datatype": "string", - "defaultValue": "*", - "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" - }, - "SkipRestore": { - "displayName": "Skip restore", - "description": "If specified, skips the automatic restore of the project on create.", - "type": "parameter", - "datatype": "bool", - "defaultValue": "false" - }, - "Namespace": { - "type": "derived", - "valueSource": "name", - "valueTransform": "safe_namespace", - "fileRename": "UmbracoPackage", - "replaces": "UmbracoPackage" - }, - "MsBuildName": { - "type": "generated", - "generator": "regex", - "dataType": "string", - "parameters": { - "source": "name", - "steps": [ - { - "regex": "\\s", - "replacement": "" - }, - { - "regex": "\\.", - "replacement": "" - }, - { - "regex": "-", - "replacement": "" - }, - { - "regex": "^[^a-zA-Z_]+", - "replacement": "" - } - ] - }, - "replaces": "UmbracoPackageMsBuild" - } - }, - "primaryOutputs": [ - { - "path": "UmbracoPackage.csproj" - } - ], - "postActions": [ - { - "id": "restore", - "condition": "(!SkipRestore)", - "description": "Restore NuGet packages required by this project.", - "manualInstructions": [ - { - "text": "Run 'dotnet restore'" - } - ], - "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", - "continueOnError": true - } - ] -} diff --git a/templates/UmbracoPackage/App_Plugins/UmbracoPackage/umbraco-package.json b/templates/UmbracoPackage/App_Plugins/UmbracoPackage/umbraco-package.json deleted file mode 100644 index 153f0b0576..0000000000 --- a/templates/UmbracoPackage/App_Plugins/UmbracoPackage/umbraco-package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "UmbracoPackage", - "name": "UmbracoPackage", - "allowPackageTelemetry": true, - "extensions": [] -} diff --git a/templates/UmbracoPackage/UmbracoPackage.csproj b/templates/UmbracoPackage/UmbracoPackage.csproj deleted file mode 100644 index 9790da947c..0000000000 --- a/templates/UmbracoPackage/UmbracoPackage.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - net9.0 - enable - enable - . - UmbracoPackage - - - - UmbracoPackage - UmbracoPackage - UmbracoPackage - ... - umbraco plugin package - - - - - - - - - - - - diff --git a/templates/UmbracoPackage/buildTransitive/UmbracoPackage.targets b/templates/UmbracoPackage/buildTransitive/UmbracoPackage.targets deleted file mode 100644 index 4c376ac97b..0000000000 --- a/templates/UmbracoPackage/buildTransitive/UmbracoPackage.targets +++ /dev/null @@ -1,21 +0,0 @@ - - - $(MSBuildThisFileDirectory)..\App_Plugins\UmbracoPackage\**\*.* - - - - - - - - - - - - - - - - - - diff --git a/templates/UmbracoPackageRcl/.template.config/ide.host.json b/templates/UmbracoPackageRcl/.template.config/ide.host.json deleted file mode 100644 index 8e630f1e99..0000000000 --- a/templates/UmbracoPackageRcl/.template.config/ide.host.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/ide.host.json", - "order": 0, - "icon": "../../icon.png", - "description": { - "id": "UmbracoPackageRcl", - "text": "Umbraco Package RCL - An empty Umbraco package/plugin (Razor Class Library)." - }, - "symbolInfo": [ - { - "id": "UmbracoVersion", - "isVisible": true - }, - { - "id": "SupportPagesAndViews", - "isVisible": true, - "persistenceScope": "templateGroup" - } - ] -} diff --git a/templates/UmbracoPackageRcl/.template.config/template.json b/templates/UmbracoPackageRcl/.template.config/template.json deleted file mode 100644 index c03c860141..0000000000 --- a/templates/UmbracoPackageRcl/.template.config/template.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/template.json", - "author": "Umbraco HQ", - "classifications": [ - "Web", - "CMS", - "Umbraco", - "Package", - "Plugin", - "Razor Class Library" - ], - "name": "Umbraco Package RCL", - "description": "An empty Umbraco package/plugin (Razor Class Library).", - "groupIdentity": "Umbraco.Templates.UmbracoPackageRcl", - "identity": "Umbraco.Templates.UmbracoPackageRcl.CSharp", - "shortName": "umbracopackage-rcl", - "tags": { - "language": "C#", - "type": "project" - }, - "sourceName": "UmbracoPackage", - "defaultName": "UmbracoPackage1", - "preferNameDirectory": true, - "symbols": { - "Framework": { - "displayName": "Framework", - "description": "The target framework for the project.", - "type": "parameter", - "datatype": "choice", - "choices": [ - { - "displayName": ".NET 9.0", - "description": "Target net9.0", - "choice": "net9.0" - } - ], - "defaultValue": "net9.0", - "replaces": "net9.0" - }, - "UmbracoVersion": { - "displayName": "Umbraco version", - "description": "The version of Umbraco.Cms to add as PackageReference.", - "type": "parameter", - "datatype": "string", - "defaultValue": "*", - "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" - }, - "SkipRestore": { - "displayName": "Skip restore", - "description": "If specified, skips the automatic restore of the project on create.", - "type": "parameter", - "datatype": "bool", - "defaultValue": "false" - }, - "SupportPagesAndViews": { - "type": "parameter", - "datatype": "bool", - "defaultValue": "false", - "displayName": "Support pages and views", - "description": "Whether to support adding traditional Razor pages and Views to this library." - } - }, - "primaryOutputs": [ - { - "path": "UmbracoPackage.csproj" - } - ], - "postActions": [ - { - "id": "restore", - "condition": "(!SkipRestore)", - "description": "Restore NuGet packages required by this project.", - "manualInstructions": [ - { - "text": "Run 'dotnet restore'" - } - ], - "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", - "continueOnError": true - } - ] -} diff --git a/templates/UmbracoPackageRcl/wwwroot/umbraco-package.json b/templates/UmbracoPackageRcl/wwwroot/umbraco-package.json deleted file mode 100644 index 153f0b0576..0000000000 --- a/templates/UmbracoPackageRcl/wwwroot/umbraco-package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "UmbracoPackage", - "name": "UmbracoPackage", - "allowPackageTelemetry": true, - "extensions": [] -}