V15: Enable umbraco-extension client to auto-build (#18597)

* feat: adds autobuild of umbraco extensions created by the dotnet template

* adds file extensions to imports to follow the esmodule convention + apply formatting

* build(deps): upgrade hey-api, vite, typescript and generate new api

* chore: formatting

* clean up actions as client is now automatically being built when starting the site

* revert change with UMBRACO_VERSION_FROM_TEMPLATE

* revert if(includeExample)

* use template name

* use template name

* feat: update the way it sets the auth token

* add back in IncludeExample if

* fix: rename allowPackageTelemetry to allowTelemetry because that is what it is actually called

* `.csproj` amends

- Adds `BeforeTargets` to "BuildClient" target
- Adds "package.json" to "RestoreClient" target input
- Removes extra parameters from `npm i` command

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
This commit is contained in:
Jacob Overgaard
2025-03-11 17:14:47 +01:00
committed by GitHub
parent 2cc9d2cab0
commit df6a88b4d5
17 changed files with 410 additions and 611 deletions

View File

@@ -136,22 +136,6 @@
],
"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": [
@@ -160,11 +144,10 @@
{
"condition": "(!IncludeExample)",
"exclude": [
"[Cc]lient/src/dashboards/**",
"[Cc]lient/src/api/schemas.gen.ts"
"[Cc]lient/src/dashboards/**"
]
}
]
}
]
}
}

View File

@@ -9,13 +9,13 @@
"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",
"@hey-api/client-fetch": "^0.8.3",
"@hey-api/openapi-ts": "^0.64.10",
"@umbraco-cms/backoffice": "^UMBRACO_VERSION_FROM_TEMPLATE",
"chalk": "^5.3.0",
"chalk": "^5.4.1",
"cross-env": "^7.0.3",
"node-fetch": "^3.3.2",
"typescript": "^5.6.3",
"vite": "^5.4.9"
"typescript": "^5.8.2",
"vite": "^6.2.0"
}
}
}

View File

@@ -2,7 +2,7 @@
"id": "Umbraco.Extension",
"name": "Umbraco.Extension",
"version": "0.0.0",
"allowPackageTelemetry": true,
"allowTelemetry": true,
"extensions": [
{
"name": "Umbraco ExtensionBundle",
@@ -11,4 +11,4 @@
"js": "/App_Plugins/UmbracoExtension/umbraco-extension.js"
}
]
}
}

View File

@@ -1,6 +1,6 @@
import fetch from 'node-fetch';
import chalk from 'chalk';
import { createClient } from '@hey-api/openapi-ts';
import { createClient, defaultPlugins } from '@hey-api/openapi-ts';
// Start notifying user we are generating the TypeScript client
console.log(chalk.green("Generating OpenAPI client..."));
@@ -20,7 +20,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
console.log("Ensure your Umbraco instance is running");
console.log(`Fetching OpenAPI definition from ${chalk.yellow(swaggerUrl)}`);
fetch(swaggerUrl).then(response => {
fetch(swaggerUrl).then(async (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`);
@@ -31,13 +31,21 @@ fetch(swaggerUrl).then(response => {
console.log(`OpenAPI spec fetched successfully`);
console.log(`Calling ${chalk.yellow('hey-api')} to generate TypeScript client`);
createClient({
client: '@hey-api/client-fetch',
await createClient({
input: swaggerUrl,
output: 'src/api',
services: {
asClass: true,
}
plugins: [
...defaultPlugins,
'@hey-api/client-fetch',
{
name: '@hey-api/typescript',
enums: 'typescript'
},
{
name: '@hey-api/sdk',
asClass: true
}
],
});
})

View File

@@ -0,0 +1,18 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { ClientOptions } from './types.gen';
import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch';
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> = (override?: Config<DefaultClientOptions & T>) => Config<Required<DefaultClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions>({
baseUrl: 'https://localhost:44389'
}));

View File

@@ -1,6 +1,3 @@
// 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';
export * from './sdk.gen';

View File

@@ -1,391 +0,0 @@
// 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,78 @@
// This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch';
//#if(IncludeExample)
import type { PingData, PingResponse, WhatsMyNameData, WhatsMyNameResponse, WhatsTheTimeMrWolfData, WhatsTheTimeMrWolfResponse, WhoAmIData, WhoAmIResponse } from './types.gen';
//#else
import type { PingData, PingResponse } from './types.gen';
//#endif
import { client as _heyApiClient } from './client.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
/**
* You can pass arbitrary values through the `meta` object. This can be
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record<string, unknown>;
};
export class UmbracoExtensionService {
public static ping<ThrowOnError extends boolean = false>(options?: Options<PingData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<PingResponse, unknown, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/umbracoextension/api/v1/ping',
...options
});
}
//#if(IncludeExample)
public static whatsMyName<ThrowOnError extends boolean = false>(options?: Options<WhatsMyNameData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhatsMyNameResponse, unknown, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/umbracoextension/api/v1/whatsMyName',
...options
});
}
public static whatsTheTimeMrWolf<ThrowOnError extends boolean = false>(options?: Options<WhatsTheTimeMrWolfData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhatsTheTimeMrWolfResponse, unknown, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/umbracoextension/api/v1/whatsTheTimeMrWolf',
...options
});
}
public static whoAmI<ThrowOnError extends boolean = false>(options?: Options<WhoAmIData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhoAmIResponse, unknown, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/umbracoextension/api/v1/whoAmI',
...options
});
}
//#endif
}

View File

@@ -1,41 +0,0 @@
// 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

@@ -1,5 +1,5 @@
// This file is auto-generated by @hey-api/openapi-ts
//#if(IncludeExample)
//#if(IncludeExample)
export type DocumentGranularPermissionModel = {
key: string;
readonly context: string;
@@ -10,15 +10,15 @@ export type ReadOnlyUserGroupModel = {
id: number;
key: string;
name: string;
icon?: (string) | null;
startContentId?: (number) | null;
startMediaId?: (number) | null;
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)>;
allowedLanguages: Array<number>;
permissions: Array<string>;
granularPermissions: Array<DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel>;
allowedSections: Array<string>;
};
export type UnknownTypeGranularPermissionModel = {
@@ -31,77 +31,166 @@ export type UserGroupModel = {
key: string;
createDate: string;
updateDate: string;
deleteDate?: (string) | null;
deleteDate?: string | null;
readonly hasIdentity: boolean;
startMediaId?: (number) | null;
startContentId?: (number) | null;
icon?: (string) | null;
startMediaId?: number | null;
startContentId?: number | null;
icon?: string | null;
alias: string;
name?: (string) | null;
name?: string | null;
hasAccessToAllLanguages: boolean;
permissions: Array<(string)>;
granularPermissions: Array<(DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel)>;
readonly allowedSections: Array<(string)>;
permissions: Array<string>;
granularPermissions: Array<DocumentGranularPermissionModel | UnknownTypeGranularPermissionModel>;
readonly allowedSections: Array<string>;
readonly userCount: number;
readonly allowedLanguages: Array<(number)>;
readonly allowedLanguages: Array<number>;
};
export type UserKindModel = 'Default' | 'Api';
export enum UserKindModel {
DEFAULT = 'Default',
API = 'Api'
}
export type UserModel = {
id: number;
key: string;
createDate: string;
updateDate: string;
deleteDate?: (string) | null;
deleteDate?: string | null;
readonly hasIdentity: boolean;
emailConfirmedDate?: (string) | null;
invitedDate?: (string) | null;
emailConfirmedDate?: string | null;
invitedDate?: string | null;
username: string;
email: string;
rawPasswordValue?: (string) | null;
passwordConfiguration?: (string) | null;
rawPasswordValue?: string | null;
passwordConfiguration?: string | null;
isApproved: boolean;
isLockedOut: boolean;
lastLoginDate?: (string) | null;
lastPasswordChangeDate?: (string) | null;
lastLockoutDate?: (string) | null;
lastLoginDate?: string | null;
lastPasswordChangeDate?: string | null;
lastLockoutDate?: string | null;
failedPasswordAttempts: number;
comments?: (string) | null;
comments?: string | null;
userState: UserStateModel;
name?: (string) | null;
readonly allowedSections: Array<(string)>;
readonly profileData: (UserModel | UserProfileModel);
securityStamp?: (string) | null;
avatar?: (string) | null;
name?: string | null;
readonly allowedSections: Array<string>;
profileData: UserModel | UserProfileModel;
securityStamp?: string | null;
avatar?: string | null;
sessionTimeout: number;
startContentIds?: Array<(number)> | null;
startMediaIds?: Array<(number)> | null;
language?: (string) | null;
startContentIds?: Array<number> | null;
startMediaIds?: Array<number> | null;
language?: string | null;
kind: UserKindModel;
readonly groups: Array<(ReadOnlyUserGroupModel | UserGroupModel)>;
readonly groups: Array<ReadOnlyUserGroupModel | UserGroupModel>;
};
export type UserProfileModel = {
id: number;
name?: (string) | null;
name?: string | null;
};
export type UserStateModel = 'Active' | 'Disabled' | 'LockedOut' | 'Invited' | 'Inactive' | 'All';
export enum UserStateModel {
ACTIVE = 'Active',
DISABLED = 'Disabled',
LOCKED_OUT = 'LockedOut',
INVITED = 'Invited',
INACTIVE = 'Inactive',
ALL = 'All'
}
//#endif
export type PingResponse = (string);
export type PingData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/hackclient/api/v1/ping';
};
export type PingError = (unknown);
//#if(IncludeExample)
export type WhatsMyNameResponse = (string);
export type PingErrors = {
/**
* The resource is protected and requires an authentication token
*/
401: unknown;
};
export type WhatsMyNameError = (unknown);
export type PingResponses = {
/**
* OK
*/
200: string;
};
export type WhatsTheTimeMrWolfResponse = (string);
export type PingResponse = PingResponses[keyof PingResponses];
//#if(IncludeExample)
export type WhatsMyNameData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/hackclient/api/v1/whatsMyName';
};
export type WhatsTheTimeMrWolfError = (unknown);
export type WhatsMyNameErrors = {
/**
* The resource is protected and requires an authentication token
*/
401: unknown;
};
export type WhoAmIResponse = ((UserModel));
export type WhatsMyNameResponses = {
/**
* OK
*/
200: string;
};
export type WhoAmIError = (unknown);
export type WhatsMyNameResponse = WhatsMyNameResponses[keyof WhatsMyNameResponses];
export type WhatsTheTimeMrWolfData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/hackclient/api/v1/whatsTheTimeMrWolf';
};
export type WhatsTheTimeMrWolfErrors = {
/**
* The resource is protected and requires an authentication token
*/
401: unknown;
};
export type WhatsTheTimeMrWolfResponses = {
/**
* OK
*/
200: string;
};
export type WhatsTheTimeMrWolfResponse = WhatsTheTimeMrWolfResponses[keyof WhatsTheTimeMrWolfResponses];
export type WhoAmIData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/hackclient/api/v1/whoAmI';
};
export type WhoAmIErrors = {
/**
* The resource is protected and requires an authentication token
*/
401: unknown;
};
export type WhoAmIResponses = {
/**
* OK
*/
200: UserModel;
};
export type WhoAmIResponse = WhoAmIResponses[keyof WhoAmIResponses];
//#endif
export type ClientOptions = {
baseUrl: 'https://localhost:44389' | (string & {});
};

View File

@@ -1,6 +1,6 @@
import { manifests as entrypoints } from './entrypoints/manifest';
import { manifests as entrypoints } from "./entrypoints/manifest.js";
//#if IncludeExample
import { manifests as dashboards } from './dashboards/manifest';
import { manifests as dashboards } from "./dashboards/manifest.js";
//#endif
// Job of the bundle is to collate all the manifests from different parts of the extension and load other manifests

View File

@@ -1,13 +1,24 @@
import { LitElement, css, html, customElement, state } from "@umbraco-cms/backoffice/external/lit";
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";
import {
UMB_NOTIFICATION_CONTEXT,
UmbNotificationContext,
} from "@umbraco-cms/backoffice/notification";
import {
UMB_CURRENT_USER_CONTEXT,
UmbCurrentUserModel,
} from "@umbraco-cms/backoffice/current-user";
import { UmbracoExtensionService, UserModel } from "../api/index.js";
@customElement('example-dashboard')
@customElement("example-dashboard")
export class ExampleDashboardElement extends UmbElementMixin(LitElement) {
@state()
private _yourName: string | undefined = "Press the button!";
@@ -28,7 +39,6 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) {
});
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
@@ -62,10 +72,10 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) {
data: {
headline: `You are ${this._serverUserData?.name}`,
message: `Your email is ${this._serverUserData?.email}`,
}
})
},
});
}
}
};
#onClickWhatsTheTimeMrWolf = async (ev: Event) => {
const buttonElement = ev.target as UUIButtonElement;
@@ -84,7 +94,7 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) {
this._timeFromMrWolf = new Date(data);
buttonElement.state = "success";
}
}
};
#onClickWhatsMyName = async (ev: Event) => {
const buttonElement = ev.target as UUIButtonElement;
@@ -100,77 +110,111 @@ export class ExampleDashboardElement extends UmbElementMixin(LitElement) {
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="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 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="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>
<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;
}
: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);
}
uui-box {
margin-bottom: var(--uui-size-layout-1);
}
h2 {
margin-top:0;
}
h2 {
margin-top: 0;
}
.wide {
grid-column: span 3;
}
`];
.wide {
grid-column: span 3;
}
`,
];
}
export default ExampleDashboardElement;
declare global {
interface HTMLElementTagNameMap {
'example-dashboard': ExampleDashboardElement;
"example-dashboard": ExampleDashboardElement;
}
}

View File

@@ -2,17 +2,17 @@ export const manifests: Array<UmbExtensionManifest> = [
{
name: "Umbraco ExtensionDashboard",
alias: "Umbraco.Extension.Dashboard",
type: 'dashboard',
js: () => import("./dashboard.element"),
type: "dashboard",
js: () => import("./dashboard.element.js"),
meta: {
label: "Example Dashboard",
pathname: "example-dashboard"
pathname: "example-dashboard",
},
conditions: [
{
alias: 'Umb.Condition.SectionAlias',
match: 'Umb.Section.Content',
}
alias: "Umb.Condition.SectionAlias",
match: "Umb.Section.Content",
},
],
}
},
];

View File

@@ -1,38 +1,31 @@
import { UmbEntryPointOnInit, UmbEntryPointOnUnload } from '@umbraco-cms/backoffice/extension-api';
import {
UmbEntryPointOnInit,
UmbEntryPointOnUnload,
} from "@umbraco-cms/backoffice/extension-api";
//#if IncludeExample
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { client } from '../api';
import { UMB_AUTH_CONTEXT } from "@umbraco-cms/backoffice/auth";
import { client } from "../api/client.gen.js";
//#endif
// load up the manifests here
export const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => {
console.log('Hello from my extension 🎉');
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({
auth: config.token,
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;
credentials: config.credentials,
});
});
//#endif
};
export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => {
console.log('Goodbye from my extension 👋');
console.log("Goodbye from my extension 👋");
};

View File

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

View File

@@ -13,5 +13,5 @@ export default defineConfig({
rollupOptions: {
external: [/^@umbraco/],
},
}
},
});

View File

@@ -24,13 +24,34 @@
<PackageReference Include="Umbraco.Cms.Api.Common" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
<PackageReference Include="Umbraco.Cms.Api.Management" Version="UMBRACO_VERSION_FROM_TEMPLATE" />
</ItemGroup>
<ItemGroup>
<ClientAssetsInputs Include="Client\**" Exclude="$(DefaultItemExcludes)" />
<!-- 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>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<!-- Restore and build Client files -->
<Target Name="RestoreClient" Inputs="Client\package.json;Client\package-lock.json" Outputs="Client\node_modules\.package-lock.json">
<Message Importance="high" Text="Restoring Client NPM packages..." />
<Exec Command="npm i" WorkingDirectory="Client" />
</Target>
<Target Name="BuildClient" BeforeTargets="AssignTargetPaths" DependsOnTargets="RestoreClient" Inputs="@(ClientAssetsInputs)" Outputs="$(IntermediateOutputPath)client.complete.txt">
<Message Importance="high" Text="Executing Client NPM build script..." />
<Exec Command="npm run build" WorkingDirectory="Client" />
<ItemGroup>
<_ClientAssetsBuildOutput Include="wwwroot\App_Plugins\**" />
</ItemGroup>
<WriteLinesToFile File="$(IntermediateOutputPath)client.complete.txt" Lines="@(_ClientAssetsBuildOutput)" Overwrite="true" />
</Target>
</Project>