[V15] Updated dotnet template for Umbraco Packages with Bellisima (#17108)

* [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 <LottePitcher@users.noreply.github.com>

* 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 <leekelleher@gmail.com>
Co-authored-by: Lotte Pitcher <LottePitcher@users.noreply.github.com>
Co-authored-by: Lotte Pitcher <github@lottepitcher.co.uk>
This commit is contained in:
Warren Buckley
2024-11-08 06:55:43 +00:00
committed by GitHub
parent 79059d60f5
commit 2f4b198ad3
35 changed files with 1375 additions and 313 deletions

View File

@@ -16,10 +16,9 @@
<Link>UmbracoProject\Program.cs</Link>
<PackagePath>UmbracoProject</PackagePath>
</Content>
<Content Include="UmbracoPackage\**" Exclude="bin;obj" />
<Content Include="UmbracoPackageRcl\**" Exclude="bin;obj" />
<Content Include="UmbracoProject\**" Exclude="bin;obj" />
<Content Include="UmbracoDockerCompose\**" Exclude="bin;obj"/>
<Content Include="UmbracoExtension\**" Exclude="bin;obj" />
<Content Include="UmbracoDockerCompose\**" Exclude="bin;obj" />
<Content Include="..\src\Umbraco.Web.UI\Views\Partials\blocklist\**">
<Link>UmbracoProject\Views\Partials\blocklist\%(RecursiveDir)%(Filename)%(Extension)</Link>
<PackagePath>UmbracoProject\Views\Partials\blocklist</PackagePath>
@@ -40,6 +39,13 @@
<ItemGroup>
<Content Update="**\.template.config\template.json" Pack="false" />
</ItemGroup>
<!-- Added project references as the sample API in umbraco-extension will fail -->
<ItemGroup>
<ProjectReference Include="..\src\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
<ProjectReference Include="..\src\Umbraco.Cms.Api.Management\Umbraco.Cms.Api.Management.csproj" />
<ProjectReference Include="..\src\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
<ProjectReference Include="..\src\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
</ItemGroup>
<Target Name="GetUpdatedTemplateJsonPackageFiles" BeforeTargets="GenerateNuspec" AfterTargets="GetUmbracoBuildVersion">
<ItemGroup>
<_TemplateJsonFiles Include="**\.template.config\template.json" Exclude="bin\**;obj\**" />

View File

@@ -17,6 +17,14 @@
"SupportPagesAndViews": {
"longName": "support-pages-and-views",
"shortName": "s"
},
"IncludeExample": {
"longName": "include-example",
"shortName": "ex"
},
"SiteDomain": {
"longName": "site-domain",
"shortName": "sd"
}
}
}

View File

@@ -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"
}
}
]
}

View File

@@ -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.

View File

@@ -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"
]
}
]
}
]
}

View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"runem.lit-plugin"
]
}

View File

@@ -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"
}
}

View File

@@ -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"
}
]
}

View File

@@ -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')}`);
});

View File

@@ -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';

View File

@@ -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;

View File

@@ -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<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
return (options?.client ?? client).get<PingResponse, PingError, ThrowOnError>({
...options,
url: '/umbraco/umbracoextension/api/v1/ping'
});
}
//#if(IncludeExample)
public static whatsMyName<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
return (options?.client ?? client).get<WhatsMyNameResponse, WhatsMyNameError, ThrowOnError>({
...options,
url: '/umbraco/umbracoextension/api/v1/whatsMyName'
});
}
public static whatsTheTimeMrWolf<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
return (options?.client ?? client).get<WhatsTheTimeMrWolfResponse, WhatsTheTimeMrWolfError, ThrowOnError>({
...options,
url: '/umbraco/umbracoextension/api/v1/whatsTheTimeMrWolf'
});
}
public static whoAmI<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
return (options?.client ?? client).get<WhoAmIResponse, WhoAmIError, ThrowOnError>({
...options,
url: '/umbraco/umbracoextension/api/v1/whoAmI'
});
}
//#endif
}

View File

@@ -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

View File

@@ -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<UmbExtensionManifest> = [
...entrypoints,
//#if IncludeExample
...dashboards,
//#endif
];

View File

@@ -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`
<uui-box headline="Who am I?">
<div slot="header">[Server]</div>
<h2><uui-icon name="icon-user"></uui-icon>${this._serverUserData?.email ? this._serverUserData.email : 'Press the button!'}</h2>
<ul>
${this._serverUserData?.groups.map(group => html`<li>${group.name}</li>`)}
</ul>
<uui-button color="default" look="primary" @click="${this.#onClickWhoAmI}">
Who am I?
</uui-button>
<p>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.</p>
</uui-box>
<uui-box headline="What's my Name?">
<div slot="header">[Server]</div>
<h2><uui-icon name="icon-user"></uui-icon> ${this._yourName }</h2>
<uui-button color="default" look="primary" @click="${this.#onClickWhatsMyName}">
Whats my name?
</uui-button>
<p>This endpoint has a forced delay to show the button 'waiting' state for a few seconds before completing the request.</p>
</uui-box>
<uui-box headline="What's the Time?">
<div slot="header">[Server]</div>
<h2><uui-icon name="icon-alarm-clock"></uui-icon> ${this._timeFromMrWolf ? this._timeFromMrWolf.toLocaleString() : 'Press the button!'}</h2>
<uui-button color="default" look="primary" @click="${this.#onClickWhatsTheTimeMrWolf}">
Whats the time Mr Wolf?
</uui-button>
<p>This endpoint gets the current date and time from the server.</p>
</uui-box>
<uui-box headline="Who am I?" class="wide">
<div slot="header">[Context]</div>
<p>Current user email: <b>${this._contextCurrentUser?.email}</b></p>
<p>This is the JSON object available by consuming the 'UMB_CURRENT_USER_CONTEXT' context:</p>
<umb-code-block language="json" copy>${JSON.stringify(this._contextCurrentUser, null, 2)}</umb-code-block>
</uui-box>
`;
}
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;
}
}

View File

@@ -0,0 +1,18 @@
export const manifests: Array<UmbExtensionManifest> = [
{
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',
}
],
}
];

View File

@@ -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 👋');
};

View File

@@ -0,0 +1,8 @@
export const manifests: Array<UmbExtensionManifest> = [
{
name: "Umbraco ExtensionEntrypoint",
alias: "Umbraco.Extension.Entrypoint",
type: "backofficeEntryPoint",
js: () => import("./entrypoint"),
}
];

View File

@@ -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" ]
}

View File

@@ -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/],
},
}
});

View File

@@ -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<IOperationIdHandler, CustomOperationHandler>();
builder.Services.Configure<SwaggerGenOptions>(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<UmbracoExtensionOperationSecurityFilter>();
});
}
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> 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"]}";
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Umbraco.Extension
{
public class Constants
{
public const string ApiName = "umbracoextension";
}
}

View File

@@ -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<string>(StatusCodes.Status200OK)]
public string Ping() => "Pong";
#if IncludeExample
[HttpGet("whatsTheTimeMrWolf")]
[ProducesResponseType(typeof(DateTime), 200)]
public DateTime WhatsTheTimeMrWolf() => DateTime.Now;
[HttpGet("whatsMyName")]
[ProducesResponseType<string>(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<IUser>(StatusCodes.Status200OK)]
public IUser? WhoAmI() => _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
#endif
}
}

View File

@@ -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
{
}
}

View File

@@ -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

View File

@@ -4,16 +4,14 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AddRazorSupportForMvc Condition="'$(SupportPagesAndViews)' == 'True'">true</AddRazorSupportForMvc>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">UmbracoPackage</RootNamespace>
<StaticWebAssetBasePath>App_Plugins/UmbracoPackage</StaticWebAssetBasePath>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">Umbraco.Extension</RootNamespace>
<StaticWebAssetBasePath>/</StaticWebAssetBasePath>
</PropertyGroup>
<PropertyGroup>
<PackageId>UmbracoPackage</PackageId>
<Product>UmbracoPackage</Product>
<Title>UmbracoPackage</Title>
<Description>...</Description>
<PackageTags>umbraco plugin package</PackageTags>
<PackageId>Umbraco.Extension</PackageId>
<Product>Umbraco.Extension</Product>
<Title>Umbraco.Extension</Title>
</PropertyGroup>
<ItemGroup Condition="'$(SupportPagesAndViews)' == 'True'">
@@ -23,5 +21,16 @@
<ItemGroup>
<PackageReference Include="Umbraco.Cms.Web.Website" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
<PackageReference Include="Umbraco.Cms.Web.Common" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
<PackageReference Include="Umbraco.Cms.Api.Common" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
<PackageReference Include="Umbraco.Cms.Api.Management" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
</ItemGroup>
<ItemGroup>
<!-- Dont include the client folder as part of packaging nuget build -->
<Content Remove="Client\**" />
<!-- However make the Umbraco-package.json included for dotnet pack or nuget package and visible to the solution -->
<None Include="Client\public\umbraco-package.json" Pack="false" />
</ItemGroup>
</Project>

View File

@@ -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": ""
}
}
}

View File

@@ -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
}
]
}

View File

@@ -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
}
]
}

View File

@@ -1,6 +0,0 @@
{
"id": "UmbracoPackage",
"name": "UmbracoPackage",
"allowPackageTelemetry": true,
"extensions": []
}

View File

@@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ContentTargetFolders>.</ContentTargetFolders>
<RootNamespace Condition="'$(name)' != '$(name{-VALUE-FORMS-}safe_namespace)'">UmbracoPackage</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<PackageId>UmbracoPackage</PackageId>
<Product>UmbracoPackage</Product>
<Title>UmbracoPackage</Title>
<Description>...</Description>
<PackageTags>umbraco plugin package</PackageTags>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Umbraco.Cms.Web.Website" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
<PackageReference Include="Umbraco.Cms.Web.Common" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
</ItemGroup>
<ItemGroup>
<Content Include="App_Plugins\UmbracoPackage\**" ExcludeFromSingleFile="true" CopyToPublishDirectory="Always" />
<None Include="buildTransitive\**" Pack="true" PackagePath="buildTransitive" />
</ItemGroup>
</Project>

View File

@@ -1,21 +0,0 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<UmbracoPackageMsBuildContentFilesPath>$(MSBuildThisFileDirectory)..\App_Plugins\UmbracoPackage\**\*.*</UmbracoPackageMsBuildContentFilesPath>
</PropertyGroup>
<Target Name="CopyUmbracoPackageMsBuildAssets" BeforeTargets="BeforeBuild">
<ItemGroup>
<UmbracoPackageMsBuildContentFiles Include="$(UmbracoPackageMsBuildContentFilesPath)" />
</ItemGroup>
<Message Text="Copying UmbracoPackage files: $(UmbracoPackageMsBuildContentFilesPath) - #@(UmbracoPackageMsBuildContentFiles->Count()) files" Importance="high" />
<Copy SourceFiles="@(UmbracoPackageMsBuildContentFiles)" DestinationFiles="@(UmbracoPackageMsBuildContentFiles->'$(MSBuildProjectDirectory)\App_Plugins\UmbracoPackage\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
</Target>
<Target Name="ClearUmbracoPackageMsBuildAssets" BeforeTargets="Clean">
<ItemGroup>
<UmbracoPackageMsBuildDir Include="$(MSBuildProjectDirectory)\App_Plugins\UmbracoPackage\" />
</ItemGroup>
<Message Text="Clear old UmbracoPackage data" Importance="high" />
<RemoveDir Directories="@(UmbracoPackageMsBuildDir)" />
</Target>
</Project>

View File

@@ -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"
}
]
}

View File

@@ -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
}
]
}

View File

@@ -1,6 +0,0 @@
{
"id": "UmbracoPackage",
"name": "UmbracoPackage",
"allowPackageTelemetry": true,
"extensions": []
}