
Umbraco Backoffice
- 335 installs
- 26 repo stars
- Updated August 1, 2026
- umbraco/umbraco-cms-backoffice-skills
umbraco-backoffice is an Umbraco CMS agent skill that routes backoffice extension development across 57 extension types with working blueprints and sub-skill references for developers who customize the Umbraco backoffice
About
umbraco-backoffice is the backbone skill in the umbraco/Umbraco-CMS-Backoffice-Skills collection, which ships 58 backoffice extension skills and over 66 skills total across the repository. The skill provides a complete extension map showing where all 57 extension types appear in the Umbraco backoffice UI, working blueprints developers can copy and adapt, and categorized links to specialized sub-skills. Developers reach for umbraco-backoffice when starting a new backoffice customization, understanding how sections, menus, dashboards, workspaces, and trees connect, or selecting the right skill for a specific UI location. Runnable examples include Blueprint 1 combining six skills, TimeDashboard wiring 13 extension types, tree-example using seven skills, and notes-wiki spanning 27 skills with a C# backend.
- umbraco-backoffice
- AI & Agent Building
- AI-coding skill
Umbraco Backoffice by the numbers
- 335 all-time installs (skills.sh)
- +13 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,184 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/umbraco/umbraco-cms-backoffice-skills --skill umbraco-backofficeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 335 |
|---|---|
| repo stars | ★ 26 |
| Last updated | August 1, 2026 |
| Repository | umbraco/umbraco-cms-backoffice-skills ↗ |
How do you structure Umbraco backoffice extension types correctly?
Helps with ai & agent building tasks.
Who is it for?
Developers customizing Umbraco CMS backoffice UI who need the extension map, tested blueprints, and sub-skill routing across dozens of extension types.
Skip if: Skip umbraco-backoffice when building a non-Umbraco CMS admin UI or a public marketing site without backoffice extension work.
When should I use this skill?
Trigger umbraco-backoffice when the user starts Umbraco backoffice customization, asks where an extension type belongs, or needs blueprint examples for dashboards or workspaces.
What you get
Extension map selections, blueprint-based Umbraco backoffice extensions, and sub-skill routing for dashboards, trees, workspaces, and C# APIs.
- extension architecture map
- blueprint-based extension code
- sub-skill routing plan
By the numbers
- Maps 57 Umbraco backoffice extension types in the extension map
- Repository ships 58 backoffice extension skills with runnable blueprints
Files
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<StaticWebAssetBasePath>/</StaticWebAssetBasePath>
</PropertyGroup>
<PropertyGroup>
<PackageId>MyExtension</PackageId>
<Product>MyExtension</Product>
<Title>MyExtension</Title>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Umbraco.Cms.Web.Website" Version="17.*" />
<PackageReference Include="Umbraco.Cms.Web.Common" Version="17.*" />
<PackageReference Include="Umbraco.Cms.Api.Common" Version="17.*" />
<PackageReference Include="Umbraco.Cms.Api.Management" Version="17.*" />
</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>
{
"name": "my-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:5000/umbraco/swagger/myextension/swagger.json"
},
"devDependencies": {
"@hey-api/openapi-ts": "^0.85.0",
"@umbraco-cms/backoffice": "^*",
"chalk": "^5.6.0",
"cross-env": "^10.0.0",
"node-fetch": "^3.3.2",
"typescript": "^5.9.2",
"vite": "^7.1.3"
}
}{
"id": "MyExtension",
"name": "MyExtension",
"version": "0.0.0",
"allowTelemetry": true,
"extensions": [
{
"name": "My Extension Bundle",
"alias": "MyExtension.Bundle",
"type": "bundle",
"js": "/App_Plugins/MyExtension/my-extension.js"
}
]
}import fetch from 'node-fetch';
import chalk from 'chalk';
import { createClient, defaultPlugins } 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(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`);
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`);
await createClient({
input: swaggerUrl,
output: 'src/api',
plugins: [
...defaultPlugins,
'@hey-api/client-fetch',
{
name: '@hey-api/sdk',
asClass: true,
classNameBuilder: '{{name}}Service',
}
],
});
})
.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')}`);
});
// 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 './client';
/**
* 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'
}));// This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from '../core/types.gen';
import type {
Client,
Config,
RequestOptions,
ResolvedRequestOptions,
} from './types.gen';
import {
buildUrl,
createConfig,
createInterceptors,
getParseAs,
mergeConfigs,
mergeHeaders,
setAuthParams,
} from './utils.gen';
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any;
headers: ReturnType<typeof mergeHeaders>;
};
export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config);
return getConfig();
};
const interceptors = createInterceptors<
Request,
Response,
unknown,
ResolvedRequestOptions
>();
const beforeRequest = async (options: RequestOptions) => {
const opts = {
..._config,
...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined,
};
if (opts.security) {
await setAuthParams({
...opts,
security: opts.security,
});
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
}
if (opts.body && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body);
}
// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.serializedBody === undefined || opts.serializedBody === '') {
opts.headers.delete('Content-Type');
}
const url = buildUrl(opts);
return { opts, url };
};
const request: Client['request'] = async (options) => {
// @ts-expect-error
const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
body: opts.serializedBody,
};
let request = new Request(url, requestInit);
for (const fn of interceptors.request._fns) {
if (fn) {
request = await fn(request, opts);
}
}
// fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!;
let response = await _fetch(request);
for (const fn of interceptors.response._fns) {
if (fn) {
response = await fn(response, request, opts);
}
}
const result = {
request,
response,
};
if (response.ok) {
if (
response.status === 204 ||
response.headers.get('Content-Length') === '0'
) {
return opts.responseStyle === 'data'
? {}
: {
data: {},
...result,
};
}
const parseAs =
(opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
let data: any;
switch (parseAs) {
case 'arrayBuffer':
case 'blob':
case 'formData':
case 'json':
case 'text':
data = await response[parseAs]();
break;
case 'stream':
return opts.responseStyle === 'data'
? response.body
: {
data: response.body,
...result,
};
}
if (parseAs === 'json') {
if (opts.responseValidator) {
await opts.responseValidator(data);
}
if (opts.responseTransformer) {
data = await opts.responseTransformer(data);
}
}
return opts.responseStyle === 'data'
? data
: {
data,
...result,
};
}
const textError = await response.text();
let jsonError: unknown;
try {
jsonError = JSON.parse(textError);
} catch {
// noop
}
const error = jsonError ?? textError;
let finalError = error;
for (const fn of interceptors.error._fns) {
if (fn) {
finalError = (await fn(error, response, request, opts)) as string;
}
}
finalError = finalError || ({} as string);
if (opts.throwOnError) {
throw finalError;
}
// TODO: we probably want to return error and improve types
return opts.responseStyle === 'data'
? undefined
: {
error: finalError,
...result,
};
};
const makeMethodFn =
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeSseFn =
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
const { opts, url } = await beforeRequest(options);
return createSseClient({
...opts,
body: opts.body as BodyInit | null | undefined,
headers: opts.headers as unknown as Record<string, string>,
method,
url,
});
};
return {
buildUrl,
connect: makeMethodFn('CONNECT'),
delete: makeMethodFn('DELETE'),
get: makeMethodFn('GET'),
getConfig,
head: makeMethodFn('HEAD'),
interceptors,
options: makeMethodFn('OPTIONS'),
patch: makeMethodFn('PATCH'),
post: makeMethodFn('POST'),
put: makeMethodFn('PUT'),
request,
setConfig,
sse: {
connect: makeSseFn('CONNECT'),
delete: makeSseFn('DELETE'),
get: makeSseFn('GET'),
head: makeSseFn('HEAD'),
options: makeSseFn('OPTIONS'),
patch: makeSseFn('PATCH'),
post: makeSseFn('POST'),
put: makeSseFn('PUT'),
trace: makeSseFn('TRACE'),
},
trace: makeMethodFn('TRACE'),
} as Client;
};
// This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from '../core/auth.gen';
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
export {
formDataBodySerializer,
jsonBodySerializer,
urlSearchParamsBodySerializer,
} from '../core/bodySerializer.gen';
export { buildClientParams } from '../core/params.gen';
export { createClient } from './client.gen';
export type {
Client,
ClientOptions,
Config,
CreateClientConfig,
Options,
OptionsLegacyParser,
RequestOptions,
RequestResult,
ResolvedRequestOptions,
ResponseStyle,
TDataShape,
} from './types.gen';
export { createConfig, mergeHeaders } from './utils.gen';
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from '../core/auth.gen';
import type {
ServerSentEventsOptions,
ServerSentEventsResult,
} from '../core/serverSentEvents.gen';
import type {
Client as CoreClient,
Config as CoreConfig,
} from '../core/types.gen';
import type { Middleware } from './utils.gen';
export type ResponseStyle = 'data' | 'fields';
export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
CoreConfig {
/**
* Base URL for all requests made by this client.
*/
baseUrl?: T['baseUrl'];
/**
* Fetch API implementation. You can use this option to provide a custom
* fetch instance.
*
* @default globalThis.fetch
*/
fetch?: (request: Request) => ReturnType<typeof fetch>;
/**
* Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect.
*
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/
next?: never;
/**
* Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all.
*
* @default 'auto'
*/
parseAs?:
| 'arrayBuffer'
| 'auto'
| 'blob'
| 'formData'
| 'json'
| 'stream'
| 'text';
/**
* Should we return only data or multiple fields (data, error, response, etc.)?
*
* @default 'fields'
*/
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T['throwOnError'];
}
export interface RequestOptions<
TData = unknown,
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends Config<{
responseStyle: TResponseStyle;
throwOnError: ThrowOnError;
}>,
Pick<
ServerSentEventsOptions<TData>,
| 'onSseError'
| 'onSseEvent'
| 'sseDefaultRetryDelay'
| 'sseMaxRetryAttempts'
| 'sseMaxRetryDelay'
> {
/**
* Any body that you want to add to your request.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
*/
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
/**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
}
export interface ResolvedRequestOptions<
TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean,
Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
serializedBody?: string;
}
export type RequestResult<
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = ThrowOnError extends true
? Promise<
TResponseStyle extends 'data'
? TData extends Record<string, unknown>
? TData[keyof TData]
: TData
: {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
request: Request;
response: Response;
}
>
: Promise<
TResponseStyle extends 'data'
?
| (TData extends Record<string, unknown>
? TData[keyof TData]
: TData)
| undefined
: (
| {
data: TData extends Record<string, unknown>
? TData[keyof TData]
: TData;
error: undefined;
}
| {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions {
baseUrl?: string;
responseStyle?: ResponseStyle;
throwOnError?: boolean;
}
type MethodFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type SseFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => Promise<ServerSentEventsResult<TData, TError>>;
type RequestFn = <
TData = unknown,
TError = unknown,
ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = 'fields',
>(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
Pick<
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
'method'
>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = <
TData extends {
body?: unknown;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
url: string;
},
>(
options: Pick<TData, 'url'> & Options<TData>,
) => string;
export type Client = CoreClient<
RequestFn,
Config,
MethodFn,
BuildUrlFn,
SseFn
> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
};
/**
* 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 ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export interface TDataShape {
body?: unknown;
headers?: unknown;
path?: unknown;
query?: unknown;
url: string;
}
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options<
TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean,
TResponse = unknown,
TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url'
> &
Omit<TData, 'url'>;
export type OptionsLegacyParser<
TData = unknown,
ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = 'fields',
> = TData extends { body?: any }
? TData extends { headers?: any }
? OmitKeys<
RequestOptions<unknown, TResponseStyle, ThrowOnError>,
'body' | 'headers' | 'url'
> &
TData
: OmitKeys<
RequestOptions<unknown, TResponseStyle, ThrowOnError>,
'body' | 'url'
> &
TData &
Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'headers'>
: TData extends { headers?: any }
? OmitKeys<
RequestOptions<unknown, TResponseStyle, ThrowOnError>,
'headers' | 'url'
> &
TData &
Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'body'>
: OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'url'> &
TData;
// This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from '../core/auth.gen';
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
import { jsonBodySerializer } from '../core/bodySerializer.gen';
import {
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer.gen';
import { getUrl } from '../core/utils.gen';
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
export const createQuerySerializer = <T = unknown>({
allowReserved,
array,
object,
}: QuerySerializerOptions = {}) => {
const querySerializer = (queryParams: T) => {
const search: string[] = [];
if (queryParams && typeof queryParams === 'object') {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({
allowReserved,
explode: true,
name,
style: 'form',
value,
...array,
});
if (serializedArray) search.push(serializedArray);
} else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({
allowReserved,
explode: true,
name,
style: 'deepObject',
value: value as Record<string, unknown>,
...object,
});
if (serializedObject) search.push(serializedObject);
} else {
const serializedPrimitive = serializePrimitiveParam({
allowReserved,
name,
value: value as string,
});
if (serializedPrimitive) search.push(serializedPrimitive);
}
}
}
return search.join('&');
};
return querySerializer;
};
/**
* Infers parseAs value from provided Content-Type header.
*/
export const getParseAs = (
contentType: string | null,
): Exclude<Config['parseAs'], 'auto'> => {
if (!contentType) {
// If no Content-Type header is provided, the best we can do is return the raw response body,
// which is effectively the same as the 'stream' option.
return 'stream';
}
const cleanContent = contentType.split(';')[0]?.trim();
if (!cleanContent) {
return;
}
if (
cleanContent.startsWith('application/json') ||
cleanContent.endsWith('+json')
) {
return 'json';
}
if (cleanContent === 'multipart/form-data') {
return 'formData';
}
if (
['application/', 'audio/', 'image/', 'video/'].some((type) =>
cleanContent.startsWith(type),
)
) {
return 'blob';
}
if (cleanContent.startsWith('text/')) {
return 'text';
}
return;
};
const checkForExistence = (
options: Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
},
name?: string,
): boolean => {
if (!name) {
return false;
}
if (
options.headers.has(name) ||
options.query?.[name] ||
options.headers.get('Cookie')?.includes(`${name}=`)
) {
return true;
}
return false;
};
export const setAuthParams = async ({
security,
...options
}: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers;
}) => {
for (const auth of security) {
if (checkForExistence(options, auth.name)) {
continue;
}
const token = await getAuthToken(auth, options.auth);
if (!token) {
continue;
}
const name = auth.name ?? 'Authorization';
switch (auth.in) {
case 'query':
if (!options.query) {
options.query = {};
}
options.query[name] = token;
break;
case 'cookie':
options.headers.append('Cookie', `${name}=${token}`);
break;
case 'header':
default:
options.headers.set(name, token);
break;
}
}
};
export const buildUrl: Client['buildUrl'] = (options) =>
getUrl({
baseUrl: options.baseUrl as string,
path: options.path,
query: options.query,
querySerializer:
typeof options.querySerializer === 'function'
? options.querySerializer
: createQuerySerializer(options.querySerializer),
url: options.url,
});
export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b };
if (config.baseUrl?.endsWith('/')) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
}
config.headers = mergeHeaders(a.headers, b.headers);
return config;
};
export const mergeHeaders = (
...headers: Array<Required<Config>['headers'] | undefined>
): Headers => {
const mergedHeaders = new Headers();
for (const header of headers) {
if (!header || typeof header !== 'object') {
continue;
}
const iterator =
header instanceof Headers ? header.entries() : Object.entries(header);
for (const [key, value] of iterator) {
if (value === null) {
mergedHeaders.delete(key);
} else if (Array.isArray(value)) {
for (const v of value) {
mergedHeaders.append(key, v as string);
}
} else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json'
mergedHeaders.set(
key,
typeof value === 'object' ? JSON.stringify(value) : (value as string),
);
}
}
}
return mergedHeaders;
};
type ErrInterceptor<Err, Res, Req, Options> = (
error: Err,
response: Res,
request: Req,
options: Options,
) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> {
_fns: (Interceptor | null)[];
constructor() {
this._fns = [];
}
clear() {
this._fns = [];
}
getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === 'number') {
return this._fns[id] ? id : -1;
} else {
return this._fns.indexOf(id);
}
}
exists(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
return !!this._fns[index];
}
eject(id: number | Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = null;
}
}
update(id: number | Interceptor, fn: Interceptor) {
const index = this.getInterceptorIndex(id);
if (this._fns[index]) {
this._fns[index] = fn;
return id;
} else {
return false;
}
}
use(fn: Interceptor) {
this._fns = [...this._fns, fn];
return this._fns.length - 1;
}
}
// `createInterceptors()` response, meant for external use as it does not
// expose internals
export interface Middleware<Req, Res, Err, Options> {
error: Pick<
Interceptors<ErrInterceptor<Err, Res, Req, Options>>,
'eject' | 'use'
>;
request: Pick<Interceptors<ReqInterceptor<Req, Options>>, 'eject' | 'use'>;
response: Pick<
Interceptors<ResInterceptor<Res, Req, Options>>,
'eject' | 'use'
>;
}
// do not add `Middleware` as return type so we can use _fns internally
export const createInterceptors = <Req, Res, Err, Options>() => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
});
const defaultQuerySerializer = createQuerySerializer({
allowReserved: false,
array: {
explode: true,
style: 'form',
},
object: {
explode: true,
style: 'deepObject',
},
});
const defaultHeaders = {
'Content-Type': 'application/json',
};
export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer,
headers: defaultHeaders,
parseAs: 'auto',
querySerializer: defaultQuerySerializer,
...override,
});
// This file is auto-generated by @hey-api/openapi-ts
export type AuthToken = string | undefined;
export interface Auth {
/**
* Which part of the request do we use to send the auth?
*
* @default 'header'
*/
in?: 'header' | 'query' | 'cookie';
/**
* Header or query parameter name.
*
* @default 'Authorization'
*/
name?: string;
scheme?: 'basic' | 'bearer';
type: 'apiKey' | 'http';
}
export const getAuthToken = async (
auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => {
const token =
typeof callback === 'function' ? await callback(auth) : callback;
if (!token) {
return;
}
if (auth.scheme === 'bearer') {
return `Bearer ${token}`;
}
if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`;
}
return token;
};
// This file is auto-generated by @hey-api/openapi-ts
import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from './pathSerializer.gen';
export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any;
export interface QuerySerializerOptions {
allowReserved?: boolean;
array?: SerializerOptions<ArrayStyle>;
object?: SerializerOptions<ObjectStyle>;
}
const serializeFormDataPair = (
data: FormData,
key: string,
value: unknown,
): void => {
if (typeof value === 'string' || value instanceof Blob) {
data.append(key, value);
} else if (value instanceof Date) {
data.append(key, value.toISOString());
} else {
data.append(key, JSON.stringify(value));
}
};
const serializeUrlSearchParamsPair = (
data: URLSearchParams,
key: string,
value: unknown,
): void => {
if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
};
export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): FormData => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v));
} else {
serializeFormDataPair(data, key, value);
}
});
return data;
},
};
export const jsonBodySerializer = {
bodySerializer: <T>(body: T): string =>
JSON.stringify(body, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
),
};
export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
body: T,
): string => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
}
if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else {
serializeUrlSearchParamsPair(data, key, value);
}
});
return data.toString();
},
};
// This file is auto-generated by @hey-api/openapi-ts
type Slot = 'body' | 'headers' | 'path' | 'query';
export type Field =
| {
in: Exclude<Slot, 'body'>;
/**
* Field name. This is the name we want the user to see and use.
*/
key: string;
/**
* Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`.
*/
map?: string;
}
| {
in: Extract<Slot, 'body'>;
/**
* Key isn't required for bodies.
*/
key?: string;
map?: string;
};
export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>;
}
export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = {
$body_: 'body',
$headers_: 'headers',
$path_: 'path',
$query_: 'query',
};
const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map<
string,
{
in: Slot;
map?: string;
}
>;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) {
map = new Map();
}
for (const config of fields) {
if ('in' in config) {
if (config.key) {
map.set(config.key, {
in: config.in,
map: config.map,
});
}
} else if (config.args) {
buildKeyMap(config.args, map);
}
}
return map;
};
interface Params {
body: unknown;
headers: Record<string, unknown>;
path: Record<string, unknown>;
query: Record<string, unknown>;
}
const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Object.keys(value).length) {
delete params[slot as Slot];
}
}
};
export const buildClientParams = (
args: ReadonlyArray<unknown>,
fields: FieldsConfig,
) => {
const params: Params = {
body: {},
headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) {
if (fields[index]) {
config = fields[index];
}
if (!config) {
continue;
}
if ('in' in config) {
if (config.key) {
const field = map.get(config.key)!;
const name = field.map || config.key;
(params[field.in] as Record<string, unknown>)[name] = arg;
} else {
params.body = arg;
}
} else {
for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key);
if (field) {
const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value;
} else {
const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
if (extra) {
const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[
key.slice(prefix.length)
] = value;
} else {
for (const [slot, allowed] of Object.entries(
config.allowExtra ?? {},
)) {
if (allowed) {
(params[slot as Slot] as Record<string, unknown>)[key] = value;
break;
}
}
}
}
}
}
}
stripEmptySlots(params);
return params;
};
// This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializePrimitiveOptions {
allowReserved?: boolean;
name: string;
}
export interface SerializerOptions<T> {
/**
* @default true
*/
explode: boolean;
style: T;
}
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string;
}
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) {
case 'form':
return ',';
case 'pipeDelimited':
return '|';
case 'spaceDelimited':
return '%20';
default:
return ',';
}
};
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) {
case 'label':
return '.';
case 'matrix':
return ';';
case 'simple':
return ',';
default:
return '&';
}
};
export const serializeArrayParam = ({
allowReserved,
explode,
name,
style,
value,
}: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[];
}) => {
if (!explode) {
const joinedValues = (
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
).join(separatorArrayNoExplode(style));
switch (style) {
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
case 'simple':
return joinedValues;
default:
return `${name}=${joinedValues}`;
}
}
const separator = separatorArrayExplode(style);
const joinedValues = value
.map((v) => {
if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string);
}
return serializePrimitiveParam({
allowReserved,
name,
value: v as string,
});
})
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};
export const serializePrimitiveParam = ({
allowReserved,
name,
value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === 'object') {
throw new Error(
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
);
}
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
};
export const serializeObjectParam = ({
allowReserved,
explode,
name,
style,
value,
valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date;
valueOnly?: boolean;
}) => {
if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
}
if (style !== 'deepObject' && !explode) {
let values: string[] = [];
Object.entries(value).forEach(([key, v]) => {
values = [
...values,
key,
allowReserved ? (v as string) : encodeURIComponent(v as string),
];
});
const joinedValues = values.join(',');
switch (style) {
case 'form':
return `${name}=${joinedValues}`;
case 'label':
return `.${joinedValues}`;
case 'matrix':
return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value)
.map(([key, v]) =>
serializePrimitiveParam({
allowReserved,
name: style === 'deepObject' ? `${name}[${key}]` : key,
value: v as string,
}),
)
.join(separator);
return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
};
// This file is auto-generated by @hey-api/openapi-ts
import type { Config } from './types.gen';
export type ServerSentEventsOptions<TData = unknown> = Omit<
RequestInit,
'method'
> &
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
/**
* Callback invoked when a network or parsing error occurs during streaming.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param error The error that occurred.
*/
onSseError?: (error: unknown) => void;
/**
* Callback invoked when an event is streamed from the server.
*
* This option applies only if the endpoint returns a stream of events.
*
* @param event Event streamed from the server.
* @returns Nothing (void).
*/
onSseEvent?: (event: StreamEvent<TData>) => void;
/**
* Default retry delay in milliseconds.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 3000
*/
sseDefaultRetryDelay?: number;
/**
* Maximum number of retry attempts before giving up.
*/
sseMaxRetryAttempts?: number;
/**
* Maximum retry delay in milliseconds.
*
* Applies only when exponential backoff is used.
*
* This option applies only if the endpoint returns a stream of events.
*
* @default 30000
*/
sseMaxRetryDelay?: number;
/**
* Optional sleep function for retry backoff.
*
* Defaults to using `setTimeout`.
*/
sseSleepFn?: (ms: number) => Promise<void>;
url: string;
};
export interface StreamEvent<TData = unknown> {
data: TData;
event?: string;
id?: string;
retry?: number;
}
export type ServerSentEventsResult<
TData = unknown,
TReturn = void,
TNext = unknown,
> = {
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
TNext
>;
};
export const createSseClient = <TData = unknown>({
onSseError,
onSseEvent,
responseTransformer,
responseValidator,
sseDefaultRetryDelay,
sseMaxRetryAttempts,
sseMaxRetryDelay,
sseSleepFn,
url,
...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
let lastEventId: string | undefined;
const sleep =
sseSleepFn ??
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
let attempt = 0;
const signal = options.signal ?? new AbortController().signal;
while (true) {
if (signal.aborted) break;
attempt++;
const headers =
options.headers instanceof Headers
? options.headers
: new Headers(options.headers as Record<string, string> | undefined);
if (lastEventId !== undefined) {
headers.set('Last-Event-ID', lastEventId);
}
try {
const response = await fetch(url, { ...options, headers, signal });
if (!response.ok)
throw new Error(
`SSE failed: ${response.status} ${response.statusText}`,
);
if (!response.body) throw new Error('No body in SSE response');
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let buffer = '';
const abortHandler = () => {
try {
reader.cancel();
} catch {
// noop
}
};
signal.addEventListener('abort', abortHandler);
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += value;
const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? '';
for (const chunk of chunks) {
const lines = chunk.split('\n');
const dataLines: Array<string> = [];
let eventName: string | undefined;
for (const line of lines) {
if (line.startsWith('data:')) {
dataLines.push(line.replace(/^data:\s*/, ''));
} else if (line.startsWith('event:')) {
eventName = line.replace(/^event:\s*/, '');
} else if (line.startsWith('id:')) {
lastEventId = line.replace(/^id:\s*/, '');
} else if (line.startsWith('retry:')) {
const parsed = Number.parseInt(
line.replace(/^retry:\s*/, ''),
10,
);
if (!Number.isNaN(parsed)) {
retryDelay = parsed;
}
}
}
let data: unknown;
let parsedJson = false;
if (dataLines.length) {
const rawData = dataLines.join('\n');
try {
data = JSON.parse(rawData);
parsedJson = true;
} catch {
data = rawData;
}
}
if (parsedJson) {
if (responseValidator) {
await responseValidator(data);
}
if (responseTransformer) {
data = await responseTransformer(data);
}
}
onSseEvent?.({
data,
event: eventName,
id: lastEventId,
retry: retryDelay,
});
if (dataLines.length) {
yield data as any;
}
}
}
} finally {
signal.removeEventListener('abort', abortHandler);
reader.releaseLock();
}
break; // exit loop on normal completion
} catch (error) {
// connection failed or aborted; retry after delay
onSseError?.(error);
if (
sseMaxRetryAttempts !== undefined &&
attempt >= sseMaxRetryAttempts
) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(
retryDelay * 2 ** (attempt - 1),
sseMaxRetryDelay ?? 30000,
);
await sleep(backoff);
}
}
};
const stream = createStream();
return { stream };
};
// This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from './auth.gen';
import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from './bodySerializer.gen';
export type HttpMethod =
| 'connect'
| 'delete'
| 'get'
| 'head'
| 'options'
| 'patch'
| 'post'
| 'put'
| 'trace';
export type Client<
RequestFn = never,
Config = unknown,
MethodFn = never,
BuildUrlFn = never,
SseFn = never,
> = {
/**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
getConfig: () => Config;
request: RequestFn;
setConfig: (config: Config) => Config;
} & {
[K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never]
? { sse?: never }
: { sse: { [K in HttpMethod]: SseFn } });
export interface Config {
/**
* Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array.
*/
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/**
* A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used.
*/
bodySerializer?: BodySerializer | null;
/**
* An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with.
*
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/
headers?:
| RequestInit['headers']
| Record<
string,
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined
| unknown
>;
/**
* The request method.
*
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
*/
method?: Uppercase<HttpMethod>;
/**
* A function for serializing request query parameters. By default, arrays
* will be exploded in form style, objects will be exploded in deepObject
* style, and reserved characters are percent-encoded.
*
* This method will have no effect if the native `paramsSerializer()` Axios
* API function is used.
*
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
*/
querySerializer?: QuerySerializer | QuerySerializerOptions;
/**
* A function validating request data. This is useful if you want to ensure
* the request conforms to the desired shape, so it can be safely sent to
* the server.
*/
requestValidator?: (data: unknown) => Promise<unknown>;
/**
* A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
}
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? true
: [T] extends [never | undefined]
? [undefined] extends [T]
? false
: true
: false;
export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
? never
: K]: T[K];
};
// This file is auto-generated by @hey-api/openapi-ts
import type { QuerySerializer } from './bodySerializer.gen';
import {
type ArraySeparatorStyle,
serializeArrayParam,
serializeObjectParam,
serializePrimitiveParam,
} from './pathSerializer.gen';
export interface PathSerializer {
path: Record<string, unknown>;
url: string;
}
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url;
const matches = _url.match(PATH_PARAM_RE);
if (matches) {
for (const match of matches) {
let explode = false;
let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = 'simple';
if (name.endsWith('*')) {
explode = true;
name = name.substring(0, name.length - 1);
}
if (name.startsWith('.')) {
name = name.substring(1);
style = 'label';
} else if (name.startsWith(';')) {
name = name.substring(1);
style = 'matrix';
}
const value = path[name];
if (value === undefined || value === null) {
continue;
}
if (Array.isArray(value)) {
url = url.replace(
match,
serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === 'object') {
url = url.replace(
match,
serializeObjectParam({
explode,
name,
style,
value: value as Record<string, unknown>,
valueOnly: true,
}),
);
continue;
}
if (style === 'matrix') {
url = url.replace(
match,
`;${serializePrimitiveParam({
name,
value: value as string,
})}`,
);
continue;
}
const replaceValue = encodeURIComponent(
style === 'label' ? `.${value as string}` : (value as string),
);
url = url.replace(match, replaceValue);
}
}
return url;
};
export const getUrl = ({
baseUrl,
path,
query,
querySerializer,
url: _url,
}: {
baseUrl?: string;
path?: Record<string, unknown>;
query?: Record<string, unknown>;
querySerializer: QuerySerializer;
url: string;
}) => {
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? '') + pathUrl;
if (path) {
url = defaultPathSerializer({ path, url });
}
let search = query ? querySerializer(query) : '';
if (search.startsWith('?')) {
search = search.substring(1);
}
if (search) {
url += `?${search}`;
}
return url;
};
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
export * from './sdk.gen';// This file is auto-generated by @hey-api/openapi-ts
import type { Options as ClientOptions, TDataShape, Client } from './client';
import type { PingData, PingResponses, PingErrors, WhatsMyNameData, WhatsMyNameResponses, WhatsMyNameErrors, WhatsTheTimeMrWolfData, WhatsTheTimeMrWolfResponses, WhatsTheTimeMrWolfErrors, WhoAmIData, WhoAmIResponses, WhoAmIErrors } from './types.gen';
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 MyExtensionService {
public static ping<ThrowOnError extends boolean = false>(options?: Options<PingData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<PingResponses, PingErrors, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/myextension/api/v1/ping',
...options
});
}
public static whatsMyName<ThrowOnError extends boolean = false>(options?: Options<WhatsMyNameData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhatsMyNameResponses, WhatsMyNameErrors, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/myextension/api/v1/whatsMyName',
...options
});
}
public static whatsTheTimeMrWolf<ThrowOnError extends boolean = false>(options?: Options<WhatsTheTimeMrWolfData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhatsTheTimeMrWolfResponses, WhatsTheTimeMrWolfErrors, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/myextension/api/v1/whatsTheTimeMrWolf',
...options
});
}
public static whoAmI<ThrowOnError extends boolean = false>(options?: Options<WhoAmIData, ThrowOnError>) {
return (options?.client ?? _heyApiClient).get<WhoAmIResponses, WhoAmIErrors, ThrowOnError>({
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/umbraco/myextension/api/v1/whoAmI',
...options
});
}
}
// This file is auto-generated by @hey-api/openapi-ts
export type DocumentGranularPermissionModel = {
key: string;
readonly context: string;
permission: string;
};
export type DocumentPropertyValueGranularPermissionModel = {
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 | DocumentPropertyValueGranularPermissionModel | 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 | DocumentPropertyValueGranularPermissionModel | 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>;
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';
export type PingData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/myextension/api/v1/ping';
};
export type PingErrors = {
/**
* The resource is protected and requires an authentication token
*/
401: unknown;
};
export type PingResponses = {
/**
* OK
*/
200: string;
};
export type PingResponse = PingResponses[keyof PingResponses];
export type WhatsMyNameData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/myextension/api/v1/whatsMyName';
};
export type WhatsMyNameErrors = {
/**
* The resource is protected and requires an authentication token
*/
401: unknown;
};
export type WhatsMyNameResponses = {
/**
* OK
*/
200: string;
};
export type WhatsMyNameResponse = WhatsMyNameResponses[keyof WhatsMyNameResponses];
export type WhatsTheTimeMrWolfData = {
body?: never;
path?: never;
query?: never;
url: '/umbraco/myextension/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/myextension/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];
export type ClientOptions = {
baseUrl: 'https://localhost:44389' | (string & {});
};
import { manifests as entrypoints } from "./entrypoints/manifest.js";
import { manifests as dashboards } from "./dashboards/manifest.js";
import { manifests as sections } from "./sections/manifest.js";
import { manifests as workspaces } from "./workspaces/manifest.js";
// 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,
...dashboards,
...sections,
...workspaces,
];
import {
LitElement,
css,
html,
customElement,
state,
} from "@umbraco-cms/backoffice/external/lit";
import { UmbElementMixin } from "@umbraco-cms/backoffice/element-api";
@customElement("blueprint-dashboard")
export class BlueprintDashboardElement extends UmbElementMixin(LitElement) {
@state()
private _counter = 0;
#incrementCounter = () => {
this._counter++;
};
render() {
return html`
<uui-box headline="Welcome to Blueprint">
<p>
This dashboard appears when you navigate to the Blueprint section
without selecting an item from the menu.
</p>
<h3>Interactive Example</h3>
<p>Button clicked: <strong>${this._counter}</strong> times</p>
<uui-button
color="positive"
look="primary"
@click="${this.#incrementCounter}"
>
Click me!
</uui-button>
</uui-box>
`;
}
static styles = [
css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
uui-box {
max-width: 800px;
}
h3 {
margin-top: var(--uui-size-space-5);
}
p {
line-height: 1.6;
}
`,
];
}
export default BlueprintDashboardElement;
declare global {
interface HTMLElementTagNameMap {
"blueprint-dashboard": BlueprintDashboardElement;
}
}
export const manifests: Array<UmbExtensionManifest> = [
{
name: "Blueprint Dashboard",
alias: "Blueprint.Dashboard",
type: "dashboard",
js: () => import("./dashboard.element.js"),
weight: 100,
meta: {
label: "Welcome",
pathname: "welcome",
},
conditions: [
{
// Dashboard only shows in the Blueprint section
alias: "Umb.Condition.SectionAlias",
match: "Blueprint.Section",
},
],
},
];
import type {
UmbEntryPointOnInit,
UmbEntryPointOnUnload,
} from "@umbraco-cms/backoffice/extension-api";
// Entry point for the extension
// This runs when the extension is loaded into the backoffice
export const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => {
console.log("Blueprint extension loaded");
// If you have a custom API client, configure it here:
// _host.consumeContext(UMB_AUTH_CONTEXT, async (authContext) => {
// const config = authContext?.getOpenApiConfiguration();
// // Set up your API client with auth token
// });
};
export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => {
console.log("Blueprint extension unloaded");
};
export const manifests: Array<UmbExtensionManifest> = [
{
name: "Blueprint Entrypoint",
alias: "Blueprint.Entrypoint",
type: "backofficeEntryPoint",
js: () => import("./entrypoint.js"),
},
];
import type { ManifestMenu, ManifestMenuItem } from "@umbraco-cms/backoffice/menu";
import type { ManifestSectionSidebarApp } from "@umbraco-cms/backoffice/section";
// Section alias - used to link components together
const sectionAlias = "Blueprint.Section";
// Menu that appears in the sidebar
const menuManifest: ManifestMenu = {
type: "menu",
alias: "Blueprint.Menu",
name: "Blueprint Menu",
meta: {
label: "Navigation",
},
};
// Menu item that opens the workspace when clicked
const menuItemManifest: ManifestMenuItem = {
type: "menuItem",
alias: "Blueprint.MenuItem",
name: "Blueprint Menu Item",
meta: {
label: "My Item",
icon: "icon-document",
entityType: "blueprint-entity", // Links to workspace via entityType
menus: ["Blueprint.Menu"],
},
};
// Sidebar app that contains the menu
const sectionSidebarAppManifest: ManifestSectionSidebarApp = {
type: "sectionSidebarApp",
kind: "menuWithEntityActions",
alias: "Blueprint.SidebarApp",
name: "Blueprint Sidebar",
meta: {
label: "Items",
menu: "Blueprint.Menu",
},
conditions: [
{
alias: "Umb.Condition.SectionAlias",
match: sectionAlias,
},
],
};
export const manifests: Array<UmbExtensionManifest> = [
// Section - appears in top navigation
{
type: "section",
alias: sectionAlias,
name: "Blueprint Section",
weight: 100,
meta: {
label: "Blueprint",
pathname: "blueprint",
},
},
sectionSidebarAppManifest,
menuManifest,
menuItemManifest,
];
import type { BlueprintCounterContext } from './context.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const BLUEPRINT_COUNTER_CONTEXT = new UmbContextToken<BlueprintCounterContext>(
'Blueprint.WorkspaceContext.Counter',
);
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api';
import { BLUEPRINT_COUNTER_CONTEXT } from './context-token.js';
/**
* Blueprint workspace counter context.
* Demonstrates a simple context with state management.
*/
export class BlueprintCounterContext extends UmbControllerBase {
#counter = new UmbNumberState(0);
readonly counter = this.#counter.asObservable();
constructor(host: UmbControllerHost) {
super(host);
this.provideContext(BLUEPRINT_COUNTER_CONTEXT, this);
}
increment() {
this.#counter.setValue(this.#counter.value + 1);
}
reset() {
this.#counter.setValue(0);
}
override destroy() {
this.#counter.destroy();
super.destroy();
}
}
export const api = BlueprintCounterContext;import { UMB_WORKSPACE_CONDITION_ALIAS } from "@umbraco-cms/backoffice/workspace";
const workspaceAlias = "Blueprint.Workspace";
export const manifests: Array<UmbExtensionManifest> = [
{
type: "workspace",
alias: workspaceAlias,
name: "Blueprint Workspace",
element: () => import("./workspace.element.js"),
meta: {
// entityType links this workspace to menu items with the same entityType
entityType: "blueprint-entity",
},
},
{
type: 'workspaceView',
alias: 'Blueprint.WorkspaceView.Another',
name: 'Blueprint Another View',
element: () => import('./views/anotherWorkspace.element.js'),
weight: 200,
meta: {
icon: 'icon-document',
pathname: 'another',
label: 'Another'
},
conditions: [
{
alias: 'Umb.Condition.WorkspaceAlias',
match: workspaceAlias
},
],
},
{
type: 'workspaceView',
alias: 'Blueprint.WorkspaceView.Counter',
name: 'Blueprint Counter View',
element: () => import('./views/defaultWorkspace.element.js'),
weight: 100,
meta: {
icon: 'icon-calculator',
pathname: 'counter',
label: 'Counter'
},
conditions: [
{
alias: 'Umb.Condition.WorkspaceAlias',
match: workspaceAlias
},
],
},
{
type: 'workspaceContext',
name: 'Blueprint Counter Workspace Context',
alias: 'Blueprint.WorkspaceContext.Counter',
api: () => import('./context.js'),
conditions: [
{
alias: UMB_WORKSPACE_CONDITION_ALIAS,
match: workspaceAlias,
},
]
}
];
import { BLUEPRINT_COUNTER_CONTEXT } from '../context-token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('blueprint-another-workspace-view')
export class BlueprintAnotherWorkspaceView extends UmbLitElement {
#counterContext?: typeof BLUEPRINT_COUNTER_CONTEXT.TYPE;
@state()
private _count = 0;
constructor() {
super();
this.consumeContext(BLUEPRINT_COUNTER_CONTEXT, (instance) => {
this.#counterContext = instance;
this.#observeCounter();
});
}
#observeCounter(): void {
if (!this.#counterContext) return;
this.observe(this.#counterContext.counter, (count) => {
this._count = count;
});
}
override render() {
return html`
<uui-box class="uui-text">
<h1 class="uui-h2">Another View</h1>
<p class="uui-lead">Current count value: ${this._count}</p>
<p>This is another workspace view that also consumes the Counter Context.</p>
</uui-box>
`;
}
static override styles = [
UmbTextStyles,
css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
`,
];
}
export default BlueprintAnotherWorkspaceView;
declare global {
interface HTMLElementTagNameMap {
'blueprint-another-workspace-view': BlueprintAnotherWorkspaceView;
}
}import { BLUEPRINT_COUNTER_CONTEXT } from '../context-token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('blueprint-counter-workspace-view')
export class BlueprintCounterWorkspaceView extends UmbLitElement {
#counterContext?: typeof BLUEPRINT_COUNTER_CONTEXT.TYPE;
@state()
private _count = 0;
constructor() {
super();
this.consumeContext(BLUEPRINT_COUNTER_CONTEXT, (instance) => {
this.#counterContext = instance;
this.#observeCounter();
});
}
#observeCounter(): void {
if (!this.#counterContext) return;
this.observe(this.#counterContext.counter, (count) => {
this._count = count;
});
}
#handleIncrement = () => {
this.#counterContext?.increment();
};
#handleReset = () => {
this.#counterContext?.reset();
};
override render() {
return html`
<uui-box class="uui-text">
<h1 class="uui-h2">Counter Example</h1>
<p class="uui-lead">Current count value: ${this._count}</p>
<p>This workspace view consumes the Counter Context and displays the current count.</p>
<div class="actions">
<uui-button look="primary" @click=${this.#handleIncrement}>Increment</uui-button>
<uui-button look="secondary" @click=${this.#handleReset}>Reset</uui-button>
</div>
</uui-box>
`;
}
static override styles = [
UmbTextStyles,
css`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
.actions {
display: flex;
gap: var(--uui-size-space-3);
margin-top: var(--uui-size-space-4);
}
`,
];
}
export default BlueprintCounterWorkspaceView;
declare global {
interface HTMLElementTagNameMap {
'blueprint-counter-workspace-view': BlueprintCounterWorkspaceView;
}
}import { css, html, customElement } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
@customElement("blueprint-workspace")
export class BlueprintWorkspaceElement extends UmbLitElement {
override render() {
return html`
<umb-workspace-editor headline="Blueprint" alias="Blueprint.Workspace" .enforceNoFooter=${true}>
</umb-workspace-editor>
`;
}
static override styles = css`
:host {
display: block;
height: 100%;
}
`;
}
export default BlueprintWorkspaceElement;
declare global {
interface HTMLElementTagNameMap {
"blueprint-workspace": BlueprintWorkspaceElement;
}
}
{
"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" ]
}
import { defineConfig } from "vite";
export default defineConfig({
build: {
lib: {
entry: "src/bundle.manifests.ts", // Bundle registers one or more manifests
formats: ["es"],
fileName: "my-extension",
},
outDir: "../wwwroot/App_Plugins/MyExtension", // your web component will be saved in this location
emptyOutDir: true,
sourcemap: true,
rollupOptions: {
external: [/^@umbraco/],
},
},
});
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;
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 MyExtension.Composers
{
public class MyExtensionApiComposer : 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 = "My Extension Backoffice 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<MyExtensionOperationSecurityFilter>();
});
}
public class MyExtensionOperationSecurityFilter : 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("MyExtension.Controllers", comparisonType: StringComparison.InvariantCultureIgnoreCase) is true;
}
public override string Handle(ApiDescription apiDescription) => $"{apiDescription.ActionDescriptor.RouteValues["action"]}";
}
}
}
namespace MyExtension
{
public class Constants
{
public const string ApiName = "myextension";
}
}
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace MyExtension.Controllers
{
[ApiVersion("1.0")]
[ApiExplorerSettings(GroupName = "MyExtension")]
public class MyExtensionApiController : MyExtensionApiControllerBase
{
[HttpGet("ping")]
[ProducesResponseType<string>(StatusCodes.Status200OK)]
public string Ping() => "Pong";
}
}
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 MyExtension.Controllers
{
[ApiController]
[BackOfficeRoute("myextension/api/v{version:apiVersion}")]
[Authorize(Policy = AuthorizationPolicies.SectionAccessContent)]
[MapToApi(Constants.ApiName)]
public class MyExtensionApiControllerBase : ControllerBase
{
}
}
Blueprint - Section with Menu, Dashboard & Workspace
A starter template demonstrating the fundamental Umbraco backoffice extension pattern: a custom section with sidebar navigation, dashboard, and workspace.
What This Example Shows
This is the simplest complete example of a custom section. Use it as a starting point when you need:
- A new top-level navigation item in the backoffice
- A sidebar with menu items
- A dashboard that shows when nothing is selected
- A workspace that opens when clicking menu items
Extension Types Used
| Extension Type | Alias | Purpose |
|---|---|---|
| Section | Blueprint.Section | Top-level navigation in header bar |
| SectionSidebarApp | Blueprint.SidebarApp | Container for menu in sidebar |
| Menu | Blueprint.Menu | Menu container in sidebar |
| MenuItem | Blueprint.MenuItem | Clickable item that opens workspace |
| Dashboard | Blueprint.Dashboard | Welcome panel (shows when nothing selected) |
| Workspace | Blueprint.Workspace | Editing view (opens when item clicked) |
| WorkspaceView | Blueprint.WorkspaceView.* | Tabs within the workspace |
| WorkspaceContext | Blueprint.WorkspaceContext.Counter | Shared state for workspace |
Visual Structure
+--------------------------------------------------+
| Content | Media | Settings | [BLUEPRINT] | <- Section in top nav
+--------------------------------------------------+
| SIDEBAR | MAIN AREA |
| +-------------+ | +-------------------------+ |
| | Items | | | Dashboard | | <- Shows when no item selected
| | - My Item | | | "Welcome to Blueprint" | |
| +-------------+ | +-------------------------+ |
| ^ | |
| | | +-------------------------+ |
| Menu + | | Workspace | | <- Shows when item clicked
| MenuItem | | [Another] [Counter] | | (tabs = WorkspaceViews)
| | +-------------------------+ |
+--------------------------------------------------+How Components Connect
Section (alias: "Blueprint.Section")
|
+-- Dashboard
| └── conditions: SectionAlias = "Blueprint.Section"
|
+-- SectionSidebarApp
└── conditions: SectionAlias = "Blueprint.Section"
└── meta.menu: "Blueprint.Menu"
|
+-- Menu (alias: "Blueprint.Menu")
|
+-- MenuItem
└── meta.entityType: "blueprint-entity"
|
v
Workspace
└── meta.entityType: "blueprint-entity" (MUST MATCH!)
|
+-- WorkspaceView (via condition)
+-- WorkspaceContext (via condition)Key Pattern: entityType Linking
The critical connection between menu items and workspaces is the entityType:
// In MenuItem
meta: {
entityType: "blueprint-entity", // <-- This value...
menus: ["Blueprint.Menu"],
}
// In Workspace
meta: {
entityType: "blueprint-entity", // <-- ...must match this!
}When you click the MenuItem, Umbraco looks for a Workspace with matching entityType and opens it.
Project Structure
Blueprint/
├── Client/
│ ├── src/
│ │ ├── bundle.manifests.ts # Aggregates all manifests
│ │ ├── entrypoints/
│ │ │ ├── entrypoint.ts # Extension lifecycle
│ │ │ └── manifest.ts
│ │ ├── sections/
│ │ │ └── manifest.ts # Section + SidebarApp + Menu + MenuItem
│ │ ├── dashboards/
│ │ │ ├── dashboard.element.ts # Dashboard UI
│ │ │ └── manifest.ts
│ │ └── workspaces/
│ │ ├── workspace.element.ts # Workspace container
│ │ ├── context.ts # Workspace context (shared state)
│ │ ├── context-token.ts # Context token for DI
│ │ ├── manifest.ts # Workspace + Views registration
│ │ └── views/
│ │ ├── defaultWorkspace.element.ts # Counter tab
│ │ └── anotherWorkspace.element.ts # Another tab
│ │ └── api/ # Generated OpenAPI client
│ │
│ ├── package.json
│ ├── tsconfig.json
│ └── public/
│ └── umbraco-package.json
│
├── Controllers/ # C# API (if needed)
├── Composers/ # DI setup
├── Blueprint.csproj
└── wwwroot/App_Plugins/Blueprint/ # Built outputHow to Use
Building the Client
cd Client
npm install
npm run buildThis builds the client to wwwroot/App_Plugins/Blueprint/.
Running with Umbraco
1. Add a reference to Blueprint.csproj in your Umbraco project 2. Build and run 3. Navigate to the backoffice - you'll see "Blueprint" in the top navigation
File Watching (Development)
cd Client
npm run watchChanges to TypeScript files will auto-rebuild and refresh the browser.
Skills Referenced
| Skill | What It Covers |
|---|---|
umbraco-sections | Section, SectionSidebarApp registration |
umbraco-menu | Menu container |
umbraco-menu-items | MenuItem configuration |
umbraco-dashboard | Dashboard with conditions |
umbraco-workspace | Workspace, WorkspaceView, WorkspaceContext |
umbraco-conditions | SectionAlias condition |
umbraco-context-api | Workspace context pattern |
umbraco-bundle | Manifest aggregation |
umbraco-entry-point | Extension lifecycle |
Learning Path
1. Beginner: Start with sections/manifest.ts and dashboards/manifest.ts 2. Intermediate: Study workspaces/manifest.ts and understand entityType linking 3. Advanced: Explore workspaces/context.ts for state management
When to Use This Example
Use Blueprint when you need:
- New Section: Custom top-level area in backoffice
- Simple Navigation: Menu items without tree hierarchy
- Dashboard + Workspace: Welcome view and editing view
For tree navigation, see tree-example. For more extension types, see TimeDashboard. For full-stack with C# backend, see notes-wiki.
import { B as d } from "./context-token-ClgMrqlf.js";
import { UmbTextStyles as w } from "@umbraco-cms/backoffice/style";
import { html as m, css as C, state as x, customElement as y } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement as E } from "@umbraco-cms/backoffice/lit-element";
var T = Object.defineProperty, O = Object.getOwnPropertyDescriptor, l = (t) => {
throw TypeError(t);
}, v = (t, e, r, o) => {
for (var s = o > 1 ? void 0 : o ? O(e, r) : e, n = t.length - 1, p; n >= 0; n--)
(p = t[n]) && (s = (o ? p(e, r, s) : p(s)) || s);
return o && s && T(e, r, s), s;
}, c = (t, e, r) => e.has(t) || l("Cannot " + r), h = (t, e, r) => (c(t, e, "read from private field"), e.get(t)), _ = (t, e, r) => e.has(t) ? l("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, r), W = (t, e, r, o) => (c(t, e, "write to private field"), e.set(t, r), r), k = (t, e, r) => (c(t, e, "access private method"), r), a, u, f;
let i = class extends E {
constructor() {
super(), _(this, u), _(this, a), this._count = 0, this.consumeContext(d, (t) => {
W(this, a, t), k(this, u, f).call(this);
});
}
render() {
return m`
<uui-box class="uui-text">
<h1 class="uui-h2">Another View</h1>
<p class="uui-lead">Current count value: ${this._count}</p>
<p>This is another workspace view that also consumes the Counter Context.</p>
</uui-box>
`;
}
};
a = /* @__PURE__ */ new WeakMap();
u = /* @__PURE__ */ new WeakSet();
f = function() {
h(this, a) && this.observe(h(this, a).counter, (t) => {
this._count = t;
});
};
i.styles = [
w,
C`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
`
];
v([
x()
], i.prototype, "_count", 2);
i = v([
y("blueprint-another-workspace-view")
], i);
const U = i;
export {
i as BlueprintAnotherWorkspaceView,
U as default
};
//# sourceMappingURL=anotherWorkspace.element-B92XJiC-.js.map
{"version":3,"file":"anotherWorkspace.element-B92XJiC-.js","sources":["../../../Client/src/workspaces/views/anotherWorkspace.element.ts"],"sourcesContent":["import { BLUEPRINT_COUNTER_CONTEXT } from '../context-token.js';\nimport { UmbTextStyles } from '@umbraco-cms/backoffice/style';\nimport { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';\nimport { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';\n\n@customElement('blueprint-another-workspace-view')\nexport class BlueprintAnotherWorkspaceView extends UmbLitElement {\n\t#counterContext?: typeof BLUEPRINT_COUNTER_CONTEXT.TYPE;\n\n\t@state()\n\tprivate _count = 0;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.consumeContext(BLUEPRINT_COUNTER_CONTEXT, (instance) => {\n\t\t\tthis.#counterContext = instance;\n\t\t\tthis.#observeCounter();\n\t\t});\n\t}\n\n\t#observeCounter(): void {\n\t\tif (!this.#counterContext) return;\n\t\tthis.observe(this.#counterContext.counter, (count) => {\n\t\t\tthis._count = count;\n\t\t});\n\t}\n\n\toverride render() {\n\t\treturn html`\n\t\t\t<uui-box class=\"uui-text\">\n\t\t\t\t<h1 class=\"uui-h2\">Another View</h1>\n\t\t\t\t<p class=\"uui-lead\">Current count value: ${this._count}</p>\n\t\t\t\t<p>This is another workspace view that also consumes the Counter Context.</p>\n\t\t\t</uui-box>\n\t\t`;\n\t}\n\n\tstatic override styles = [\n\t\tUmbTextStyles,\n\t\tcss`\n\t\t\t:host {\n\t\t\t\tdisplay: block;\n\t\t\t\tpadding: var(--uui-size-layout-1);\n\t\t\t}\n\t\t`,\n\t];\n}\n\nexport default BlueprintAnotherWorkspaceView;\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'blueprint-another-workspace-view': BlueprintAnotherWorkspaceView;\n\t}\n}"],"names":["_counterContext","_BlueprintAnotherWorkspaceView_instances","observeCounter_fn","BlueprintAnotherWorkspaceView","UmbLitElement","__privateAdd","BLUEPRINT_COUNTER_CONTEXT","instance","__privateSet","__privateMethod","html","__privateGet","count","UmbTextStyles","css","__decorateClass","state","customElement","BlueprintAnotherWorkspaceView_default"],"mappings":";;;;;;;;;;wXAAAA,GAAAC,GAAAC;AAMO,IAAMC,IAAN,cAA4CC,EAAc;AAAA,EAMhE,cAAc;AACb,UAAA,GAPKC,EAAA,MAAAJ,CAAA,GACNI,EAAA,MAAAL,CAAA,GAGA,KAAQ,SAAS,GAIhB,KAAK,eAAeM,GAA2B,CAACC,MAAa;AAC5D,MAAAC,EAAA,MAAKR,GAAkBO,CAAA,GACvBE,EAAA,MAAKR,GAAAC,CAAA,EAAL,KAAA,IAAA;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EASS,SAAS;AACjB,WAAOQ;AAAA;AAAA;AAAA,+CAGsC,KAAK,MAAM;AAAA;AAAA;AAAA;AAAA,EAIzD;AAWD;AAvCCV,IAAA,oBAAA,QAAA;AADMC,IAAA,oBAAA,QAAA;AAcNC,IAAe,WAAS;AACvB,EAAKS,QAAKX,CAAA,KACV,KAAK,QAAQW,EAAA,MAAKX,CAAA,EAAgB,SAAS,CAACY,MAAU;AACrD,SAAK,SAASA;AAAA,EACf,CAAC;AACF;AAnBYT,EA+BI,SAAS;AAAA,EACxBU;AAAA,EACAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAMD;AAnCQC,EAAA;AAAA,EADPC,EAAA;AAAM,GAHKb,EAIJ,WAAA,UAAA,CAAA;AAJIA,IAANY,EAAA;AAAA,EADNE,EAAc,kCAAkC;AAAA,GACpCd,CAAA;AA0Cb,MAAAe,IAAef;"}import { UmbControllerBase as e } from "@umbraco-cms/backoffice/class-api";
import { UmbNumberState as s } from "@umbraco-cms/backoffice/observable-api";
import { B as r } from "./context-token-ClgMrqlf.js";
class o extends e {
constructor(t) {
super(t), this.#t = new s(0), this.counter = this.#t.asObservable(), this.provideContext(r, this);
}
#t;
increment() {
this.#t.setValue(this.#t.value + 1);
}
reset() {
this.#t.setValue(0);
}
destroy() {
this.#t.destroy(), super.destroy();
}
}
const m = o;
export {
o as BlueprintCounterContext,
m as api
};
//# sourceMappingURL=context-CQWPbpYh.js.map
{"version":3,"file":"context-CQWPbpYh.js","sources":["../../../Client/src/workspaces/context.ts"],"sourcesContent":["import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';\nimport type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';\nimport { UmbNumberState } from '@umbraco-cms/backoffice/observable-api';\nimport { BLUEPRINT_COUNTER_CONTEXT } from './context-token.js';\n\n/**\n * Blueprint workspace counter context.\n * Demonstrates a simple context with state management.\n */\nexport class BlueprintCounterContext extends UmbControllerBase {\n\t#counter = new UmbNumberState(0);\n\treadonly counter = this.#counter.asObservable();\n\n\tconstructor(host: UmbControllerHost) {\n\t\tsuper(host);\n\t\tthis.provideContext(BLUEPRINT_COUNTER_CONTEXT, this);\n\t}\n\n\tincrement() {\n\t\tthis.#counter.setValue(this.#counter.value + 1);\n\t}\n\n\treset() {\n\t\tthis.#counter.setValue(0);\n\t}\n\n\toverride destroy() {\n\t\tthis.#counter.destroy();\n\t\tsuper.destroy();\n\t}\n}\n\nexport const api = BlueprintCounterContext;"],"names":["BlueprintCounterContext","UmbControllerBase","host","#counter","UmbNumberState","BLUEPRINT_COUNTER_CONTEXT","api"],"mappings":";;;AASO,MAAMA,UAAgCC,EAAkB;AAAA,EAI9D,YAAYC,GAAyB;AACpC,UAAMA,CAAI,GAJX,KAAAC,KAAW,IAAIC,EAAe,CAAC,GAC/B,KAAS,UAAU,KAAKD,GAAS,aAAA,GAIhC,KAAK,eAAeE,GAA2B,IAAI;AAAA,EACpD;AAAA,EANAF;AAAA,EAQA,YAAY;AACX,SAAKA,GAAS,SAAS,KAAKA,GAAS,QAAQ,CAAC;AAAA,EAC/C;AAAA,EAEA,QAAQ;AACP,SAAKA,GAAS,SAAS,CAAC;AAAA,EACzB;AAAA,EAES,UAAU;AAClB,SAAKA,GAAS,QAAA,GACd,MAAM,QAAA;AAAA,EACP;AACD;AAEO,MAAMG,IAAMN;"}import { UmbContextToken as o } from "@umbraco-cms/backoffice/context-api";
const e = new o(
"Blueprint.WorkspaceContext.Counter"
);
export {
e as B
};
//# sourceMappingURL=context-token-ClgMrqlf.js.map
{"version":3,"file":"context-token-ClgMrqlf.js","sources":["../../../Client/src/workspaces/context-token.ts"],"sourcesContent":["import type { BlueprintCounterContext } from './context.js';\r\nimport { UmbContextToken } from '@umbraco-cms/backoffice/context-api';\r\n\r\nexport const BLUEPRINT_COUNTER_CONTEXT = new UmbContextToken<BlueprintCounterContext>(\r\n\t'Blueprint.WorkspaceContext.Counter',\r\n);\r\n"],"names":["BLUEPRINT_COUNTER_CONTEXT","UmbContextToken"],"mappings":";AAGO,MAAMA,IAA4B,IAAIC;AAAA,EAC5C;AACD;"}import { LitElement as c, html as h, css as d, state as m, customElement as v } from "@umbraco-cms/backoffice/external/lit";
import { UmbElementMixin as _ } from "@umbraco-cms/backoffice/element-api";
var f = Object.defineProperty, x = Object.getOwnPropertyDescriptor, p = (e) => {
throw TypeError(e);
}, l = (e, t, r, n) => {
for (var a = n > 1 ? void 0 : n ? x(t, r) : t, o = e.length - 1, s; o >= 0; o--)
(s = e[o]) && (a = (n ? s(t, r, a) : s(a)) || a);
return n && a && f(t, r, a), a;
}, g = (e, t, r) => t.has(e) || p("Cannot " + r), y = (e, t, r) => (g(e, t, "read from private field"), r ? r.call(e) : t.get(e)), E = (e, t, r) => t.has(e) ? p("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(e) : t.set(e, r), u;
let i = class extends _(c) {
constructor() {
super(...arguments), this._counter = 0, E(this, u, () => {
this._counter++;
});
}
render() {
return h`
<uui-box headline="Welcome to Blueprint">
<p>
This dashboard appears when you navigate to the Blueprint section
without selecting an item from the menu.
</p>
<h3>Interactive Example</h3>
<p>Button clicked: <strong>${this._counter}</strong> times</p>
<uui-button
color="positive"
look="primary"
@click="${y(this, u)}"
>
Click me!
</uui-button>
</uui-box>
`;
}
};
u = /* @__PURE__ */ new WeakMap();
i.styles = [
d`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
uui-box {
max-width: 800px;
}
h3 {
margin-top: var(--uui-size-space-5);
}
p {
line-height: 1.6;
}
`
];
l([
m()
], i.prototype, "_counter", 2);
i = l([
v("blueprint-dashboard")
], i);
const C = i;
export {
i as BlueprintDashboardElement,
C as default
};
//# sourceMappingURL=dashboard.element--L2h73vx.js.map
{"version":3,"file":"dashboard.element--L2h73vx.js","sources":["../../../Client/src/dashboards/dashboard.element.ts"],"sourcesContent":["import {\r\n LitElement,\r\n css,\r\n html,\r\n customElement,\r\n state,\r\n} from \"@umbraco-cms/backoffice/external/lit\";\r\nimport { UmbElementMixin } from \"@umbraco-cms/backoffice/element-api\";\r\n\r\n@customElement(\"blueprint-dashboard\")\r\nexport class BlueprintDashboardElement extends UmbElementMixin(LitElement) {\r\n @state()\r\n private _counter = 0;\r\n\r\n #incrementCounter = () => {\r\n this._counter++;\r\n };\r\n\r\n render() {\r\n return html`\r\n <uui-box headline=\"Welcome to Blueprint\">\r\n <p>\r\n This dashboard appears when you navigate to the Blueprint section\r\n without selecting an item from the menu.\r\n </p>\r\n\r\n <h3>Interactive Example</h3>\r\n <p>Button clicked: <strong>${this._counter}</strong> times</p>\r\n <uui-button\r\n color=\"positive\"\r\n look=\"primary\"\r\n @click=\"${this.#incrementCounter}\"\r\n >\r\n Click me!\r\n </uui-button>\r\n </uui-box>\r\n `;\r\n }\r\n\r\n static styles = [\r\n css`\r\n :host {\r\n display: block;\r\n padding: var(--uui-size-layout-1);\r\n }\r\n\r\n uui-box {\r\n max-width: 800px;\r\n }\r\n\r\n h3 {\r\n margin-top: var(--uui-size-space-5);\r\n }\r\n\r\n p {\r\n line-height: 1.6;\r\n }\r\n `,\r\n ];\r\n}\r\n\r\nexport default BlueprintDashboardElement;\r\n\r\ndeclare global {\r\n interface HTMLElementTagNameMap {\r\n \"blueprint-dashboard\": BlueprintDashboardElement;\r\n }\r\n}\r\n"],"names":["_incrementCounter","BlueprintDashboardElement","UmbElementMixin","LitElement","__privateAdd","html","__privateGet","css","__decorateClass","state","customElement","BlueprintDashboardElement_default"],"mappings":";;;;;;;;uQAAAA;AAUO,IAAMC,IAAN,cAAwCC,EAAgBC,CAAU,EAAE;AAAA,EAApE,cAAA;AAAA,UAAA,GAAA,SAAA,GAEL,KAAQ,WAAW,GAEnBC,EAAA,MAAAJ,GAAoB,MAAM;AACxB,WAAK;AAAA,IACP,CAAA;AAAA,EAAA;AAAA,EAEA,SAAS;AACP,WAAOK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qCAQ0B,KAAK,QAAQ;AAAA;AAAA;AAAA;AAAA,oBAI9BC,QAAKN,CAAA,CAAiB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMxC;AAsBF;AA7CEA,IAAA,oBAAA,QAAA;AAJWC,EA6BJ,SAAS;AAAA,EACdM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBF;AA9CQC,EAAA;AAAA,EADPC,EAAA;AAAM,GADIR,EAEH,WAAA,YAAA,CAAA;AAFGA,IAANO,EAAA;AAAA,EADNE,EAAc,qBAAqB;AAAA,GACvBT,CAAA;AAmDb,MAAAU,IAAeV;"}import { B as C } from "./context-token-ClgMrqlf.js";
import { UmbTextStyles as w } from "@umbraco-cms/backoffice/style";
import { html as k, css as x, state as y, customElement as E } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement as W } from "@umbraco-cms/backoffice/lit-element";
var T = Object.defineProperty, O = Object.getOwnPropertyDescriptor, d = (t) => {
throw TypeError(t);
}, f = (t, e, r, n) => {
for (var s = n > 1 ? void 0 : n ? O(e, r) : e, c = t.length - 1, p; c >= 0; c--)
(p = t[c]) && (s = (n ? p(e, r, s) : p(s)) || s);
return n && s && T(e, r, s), s;
}, v = (t, e, r) => e.has(t) || d("Cannot " + r), a = (t, e, r) => (v(t, e, "read from private field"), r ? r.call(t) : e.get(t)), u = (t, e, r) => e.has(t) ? d("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, r), b = (t, e, r, n) => (v(t, e, "write to private field"), e.set(t, r), r), B = (t, e, r) => (v(t, e, "access private method"), r), i, l, m, h, _;
let o = class extends W {
constructor() {
super(), u(this, l), u(this, i), this._count = 0, u(this, h, () => {
a(this, i)?.increment();
}), u(this, _, () => {
a(this, i)?.reset();
}), this.consumeContext(C, (t) => {
b(this, i, t), B(this, l, m).call(this);
});
}
render() {
return k`
<uui-box class="uui-text">
<h1 class="uui-h2">Counter Example</h1>
<p class="uui-lead">Current count value: ${this._count}</p>
<p>This workspace view consumes the Counter Context and displays the current count.</p>
<div class="actions">
<uui-button look="primary" @click=${a(this, h)}>Increment</uui-button>
<uui-button look="secondary" @click=${a(this, _)}>Reset</uui-button>
</div>
</uui-box>
`;
}
};
i = /* @__PURE__ */ new WeakMap();
l = /* @__PURE__ */ new WeakSet();
m = function() {
a(this, i) && this.observe(a(this, i).counter, (t) => {
this._count = t;
});
};
h = /* @__PURE__ */ new WeakMap();
_ = /* @__PURE__ */ new WeakMap();
o.styles = [
w,
x`
:host {
display: block;
padding: var(--uui-size-layout-1);
}
.actions {
display: flex;
gap: var(--uui-size-space-3);
margin-top: var(--uui-size-space-4);
}
`
];
f([
y()
], o.prototype, "_count", 2);
o = f([
E("blueprint-counter-workspace-view")
], o);
const U = o;
export {
o as BlueprintCounterWorkspaceView,
U as default
};
//# sourceMappingURL=defaultWorkspace.element-Dj1vi3Oe.js.map
{"version":3,"file":"defaultWorkspace.element-Dj1vi3Oe.js","sources":["../../../Client/src/workspaces/views/defaultWorkspace.element.ts"],"sourcesContent":["import { BLUEPRINT_COUNTER_CONTEXT } from '../context-token.js';\nimport { UmbTextStyles } from '@umbraco-cms/backoffice/style';\nimport { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';\nimport { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';\n\n@customElement('blueprint-counter-workspace-view')\nexport class BlueprintCounterWorkspaceView extends UmbLitElement {\n\t#counterContext?: typeof BLUEPRINT_COUNTER_CONTEXT.TYPE;\n\n\t@state()\n\tprivate _count = 0;\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.consumeContext(BLUEPRINT_COUNTER_CONTEXT, (instance) => {\n\t\t\tthis.#counterContext = instance;\n\t\t\tthis.#observeCounter();\n\t\t});\n\t}\n\n\t#observeCounter(): void {\n\t\tif (!this.#counterContext) return;\n\t\tthis.observe(this.#counterContext.counter, (count) => {\n\t\t\tthis._count = count;\n\t\t});\n\t}\n\n\t#handleIncrement = () => {\n\t\tthis.#counterContext?.increment();\n\t};\n\n\t#handleReset = () => {\n\t\tthis.#counterContext?.reset();\n\t};\n\n\toverride render() {\n\t\treturn html`\n\t\t\t<uui-box class=\"uui-text\">\n\t\t\t\t<h1 class=\"uui-h2\">Counter Example</h1>\n\t\t\t\t<p class=\"uui-lead\">Current count value: ${this._count}</p>\n\t\t\t\t<p>This workspace view consumes the Counter Context and displays the current count.</p>\n\t\t\t\t<div class=\"actions\">\n\t\t\t\t\t<uui-button look=\"primary\" @click=${this.#handleIncrement}>Increment</uui-button>\n\t\t\t\t\t<uui-button look=\"secondary\" @click=${this.#handleReset}>Reset</uui-button>\n\t\t\t\t</div>\n\t\t\t</uui-box>\n\t\t`;\n\t}\n\n\tstatic override styles = [\n\t\tUmbTextStyles,\n\t\tcss`\n\t\t\t:host {\n\t\t\t\tdisplay: block;\n\t\t\t\tpadding: var(--uui-size-layout-1);\n\t\t\t}\n\t\t\t.actions {\n\t\t\t\tdisplay: flex;\n\t\t\t\tgap: var(--uui-size-space-3);\n\t\t\t\tmargin-top: var(--uui-size-space-4);\n\t\t\t}\n\t\t`,\n\t];\n}\n\nexport default BlueprintCounterWorkspaceView;\n\ndeclare global {\n\tinterface HTMLElementTagNameMap {\n\t\t'blueprint-counter-workspace-view': BlueprintCounterWorkspaceView;\n\t}\n}"],"names":["_counterContext","_BlueprintCounterWorkspaceView_instances","observeCounter_fn","_handleIncrement","_handleReset","BlueprintCounterWorkspaceView","UmbLitElement","__privateAdd","__privateGet","BLUEPRINT_COUNTER_CONTEXT","instance","__privateSet","__privateMethod","html","count","UmbTextStyles","css","__decorateClass","state","customElement","BlueprintCounterWorkspaceView_default"],"mappings":";;;;;;;;;;wYAAAA,GAAAC,GAAAC,GAAAC,GAAAC;AAMO,IAAMC,IAAN,cAA4CC,EAAc;AAAA,EAMhE,cAAc;AACb,UAAA,GAPKC,EAAA,MAAAN,CAAA,GACNM,EAAA,MAAAP,CAAA,GAGA,KAAQ,SAAS,GAiBjBO,EAAA,MAAAJ,GAAmB,MAAM;AACxB,MAAAK,EAAA,MAAKR,IAAiB,UAAA;AAAA,IACvB,CAAA,GAEAO,EAAA,MAAAH,GAAe,MAAM;AACpB,MAAAI,EAAA,MAAKR,IAAiB,MAAA;AAAA,IACvB,CAAA,GAnBC,KAAK,eAAeS,GAA2B,CAACC,MAAa;AAC5D,MAAAC,EAAA,MAAKX,GAAkBU,CAAA,GACvBE,EAAA,MAAKX,GAAAC,CAAA,EAAL,KAAA,IAAA;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAiBS,SAAS;AACjB,WAAOW;AAAA;AAAA;AAAA,+CAGsC,KAAK,MAAM;AAAA;AAAA;AAAA,yCAGjBL,QAAKL,CAAA,CAAgB;AAAA,2CACnBK,QAAKJ,CAAA,CAAY;AAAA;AAAA;AAAA;AAAA,EAI3D;AAgBD;AAxDCJ,IAAA,oBAAA,QAAA;AADMC,IAAA,oBAAA,QAAA;AAcNC,IAAe,WAAS;AACvB,EAAKM,QAAKR,CAAA,KACV,KAAK,QAAQQ,EAAA,MAAKR,CAAA,EAAgB,SAAS,CAACc,MAAU;AACrD,SAAK,SAASA;AAAA,EACf,CAAC;AACF;AAEAX,IAAA,oBAAA,QAAA;AAIAC,IAAA,oBAAA,QAAA;AAzBYC,EA2CI,SAAS;AAAA,EACxBU;AAAA,EACAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWD;AApDQC,EAAA;AAAA,EADPC,EAAA;AAAM,GAHKb,EAIJ,WAAA,UAAA,CAAA;AAJIA,IAANY,EAAA;AAAA,EADNE,EAAc,kCAAkC;AAAA,GACpCd,CAAA;AA2Db,MAAAe,IAAef;"}const e = (o, n) => {
console.log("Blueprint extension loaded");
}, t = (o, n) => {
console.log("Blueprint extension unloaded");
};
export {
e as onInit,
t as onUnload
};
//# sourceMappingURL=entrypoint-CWUPkU2h.js.map
{"version":3,"file":"entrypoint-CWUPkU2h.js","sources":["../../../Client/src/entrypoints/entrypoint.ts"],"sourcesContent":["import type {\r\n UmbEntryPointOnInit,\r\n UmbEntryPointOnUnload,\r\n} from \"@umbraco-cms/backoffice/extension-api\";\r\n\r\n// Entry point for the extension\r\n// This runs when the extension is loaded into the backoffice\r\nexport const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => {\r\n console.log(\"Blueprint extension loaded\");\r\n\r\n // If you have a custom API client, configure it here:\r\n // _host.consumeContext(UMB_AUTH_CONTEXT, async (authContext) => {\r\n // const config = authContext?.getOpenApiConfiguration();\r\n // // Set up your API client with auth token\r\n // });\r\n};\r\n\r\nexport const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => {\r\n console.log(\"Blueprint extension unloaded\");\r\n};\r\n"],"names":["onInit","_host","_extensionRegistry","onUnload"],"mappings":"AAOO,MAAMA,IAA8B,CAACC,GAAOC,MAAuB;AACxE,UAAQ,IAAI,4BAA4B;AAO1C,GAEaC,IAAkC,CAACF,GAAOC,MAAuB;AAC5E,UAAQ,IAAI,8BAA8B;AAC5C;"}import { UMB_WORKSPACE_CONDITION_ALIAS as n } from "@umbraco-cms/backoffice/workspace";
const i = [
{
name: "Blueprint Entrypoint",
alias: "Blueprint.Entrypoint",
type: "backofficeEntryPoint",
js: () => import("./entrypoint-CWUPkU2h.js")
}
], a = [
{
name: "Blueprint Dashboard",
alias: "Blueprint.Dashboard",
type: "dashboard",
js: () => import("./dashboard.element--L2h73vx.js"),
weight: 100,
meta: {
label: "Welcome",
pathname: "welcome"
},
conditions: [
{
// Dashboard only shows in the Blueprint section
alias: "Umb.Condition.SectionAlias",
match: "Blueprint.Section"
}
]
}
], t = "Blueprint.Section", o = {
type: "menu",
alias: "Blueprint.Menu",
name: "Blueprint Menu",
meta: {
label: "Navigation"
}
}, s = {
type: "menuItem",
alias: "Blueprint.MenuItem",
name: "Blueprint Menu Item",
meta: {
label: "My Item",
icon: "icon-document",
entityType: "blueprint-entity",
// Links to workspace via entityType
menus: ["Blueprint.Menu"]
}
}, p = {
type: "sectionSidebarApp",
kind: "menuWithEntityActions",
alias: "Blueprint.SidebarApp",
name: "Blueprint Sidebar",
meta: {
label: "Items",
menu: "Blueprint.Menu"
},
conditions: [
{
alias: "Umb.Condition.SectionAlias",
match: t
}
]
}, r = [
// Section - appears in top navigation
{
type: "section",
alias: t,
name: "Blueprint Section",
weight: 100,
meta: {
label: "Blueprint",
pathname: "blueprint"
}
},
p,
o,
s
], e = "Blueprint.Workspace", l = [
{
type: "workspace",
alias: e,
name: "Blueprint Workspace",
element: () => import("./workspace.element-Cqpx0ovr.js"),
meta: {
// entityType links this workspace to menu items with the same entityType
entityType: "blueprint-entity"
}
},
{
type: "workspaceView",
alias: "Blueprint.WorkspaceView.Another",
name: "Blueprint Another View",
element: () => import("./anotherWorkspace.element-B92XJiC-.js"),
weight: 200,
meta: {
icon: "icon-document",
pathname: "another",
label: "Another"
},
conditions: [
{
alias: "Umb.Condition.WorkspaceAlias",
match: e
}
]
},
{
type: "workspaceView",
alias: "Blueprint.WorkspaceView.Counter",
name: "Blueprint Counter View",
element: () => import("./defaultWorkspace.element-Dj1vi3Oe.js"),
weight: 100,
meta: {
icon: "icon-calculator",
pathname: "counter",
label: "Counter"
},
conditions: [
{
alias: "Umb.Condition.WorkspaceAlias",
match: e
}
]
},
{
type: "workspaceContext",
name: "Blueprint Counter Workspace Context",
alias: "Blueprint.WorkspaceContext.Counter",
api: () => import("./context-CQWPbpYh.js"),
conditions: [
{
alias: n,
match: e
}
]
}
], c = [
...i,
...a,
...r,
...l
];
export {
c as manifests
};
//# sourceMappingURL=my-extension.js.map
{"version":3,"file":"my-extension.js","sources":["../../../Client/src/entrypoints/manifest.ts","../../../Client/src/dashboards/manifest.ts","../../../Client/src/sections/manifest.ts","../../../Client/src/workspaces/manifest.ts","../../../Client/src/bundle.manifests.ts"],"sourcesContent":["export const manifests: Array<UmbExtensionManifest> = [\r\n {\r\n name: \"Blueprint Entrypoint\",\r\n alias: \"Blueprint.Entrypoint\",\r\n type: \"backofficeEntryPoint\",\r\n js: () => import(\"./entrypoint.js\"),\r\n },\r\n];\r\n","export const manifests: Array<UmbExtensionManifest> = [\r\n {\r\n name: \"Blueprint Dashboard\",\r\n alias: \"Blueprint.Dashboard\",\r\n type: \"dashboard\",\r\n js: () => import(\"./dashboard.element.js\"),\r\n weight: 100,\r\n meta: {\r\n label: \"Welcome\",\r\n pathname: \"welcome\",\r\n },\r\n conditions: [\r\n {\r\n // Dashboard only shows in the Blueprint section\r\n alias: \"Umb.Condition.SectionAlias\",\r\n match: \"Blueprint.Section\",\r\n },\r\n ],\r\n },\r\n];\r\n","import type { ManifestMenu, ManifestMenuItem } from \"@umbraco-cms/backoffice/menu\";\r\nimport type { ManifestSectionSidebarApp } from \"@umbraco-cms/backoffice/section\";\r\n\r\n// Section alias - used to link components together\r\nconst sectionAlias = \"Blueprint.Section\";\r\n\r\n// Menu that appears in the sidebar\r\nconst menuManifest: ManifestMenu = {\r\n type: \"menu\",\r\n alias: \"Blueprint.Menu\",\r\n name: \"Blueprint Menu\",\r\n meta: {\r\n label: \"Navigation\",\r\n },\r\n};\r\n\r\n// Menu item that opens the workspace when clicked\r\nconst menuItemManifest: ManifestMenuItem = {\r\n type: \"menuItem\",\r\n alias: \"Blueprint.MenuItem\",\r\n name: \"Blueprint Menu Item\",\r\n meta: {\r\n label: \"My Item\",\r\n icon: \"icon-document\",\r\n entityType: \"blueprint-entity\", // Links to workspace via entityType\r\n menus: [\"Blueprint.Menu\"],\r\n },\r\n};\r\n\r\n// Sidebar app that contains the menu\r\nconst sectionSidebarAppManifest: ManifestSectionSidebarApp = {\r\n type: \"sectionSidebarApp\",\r\n kind: \"menuWithEntityActions\",\r\n alias: \"Blueprint.SidebarApp\",\r\n name: \"Blueprint Sidebar\",\r\n meta: {\r\n label: \"Items\",\r\n menu: \"Blueprint.Menu\",\r\n },\r\n conditions: [\r\n {\r\n alias: \"Umb.Condition.SectionAlias\",\r\n match: sectionAlias,\r\n },\r\n ],\r\n};\r\n\r\nexport const manifests: Array<UmbExtensionManifest> = [\r\n // Section - appears in top navigation\r\n {\r\n type: \"section\",\r\n alias: sectionAlias,\r\n name: \"Blueprint Section\",\r\n weight: 100,\r\n meta: {\r\n label: \"Blueprint\",\r\n pathname: \"blueprint\",\r\n },\r\n },\r\n sectionSidebarAppManifest,\r\n menuManifest,\r\n menuItemManifest,\r\n];\r\n","import { UMB_WORKSPACE_CONDITION_ALIAS } from \"@umbraco-cms/backoffice/workspace\";\r\n\r\nconst workspaceAlias = \"Blueprint.Workspace\";\r\n\r\nexport const manifests: Array<UmbExtensionManifest> = [\r\n {\r\n type: \"workspace\",\r\n alias: workspaceAlias,\r\n name: \"Blueprint Workspace\",\r\n element: () => import(\"./workspace.element.js\"),\r\n meta: {\r\n // entityType links this workspace to menu items with the same entityType\r\n entityType: \"blueprint-entity\",\r\n },\r\n },\r\n {\r\n type: 'workspaceView',\r\n alias: 'Blueprint.WorkspaceView.Another',\r\n name: 'Blueprint Another View',\r\n element: () => import('./views/anotherWorkspace.element.js'),\r\n weight: 200,\r\n meta: {\r\n icon: 'icon-document',\r\n pathname: 'another',\r\n label: 'Another'\r\n },\r\n conditions: [\r\n {\r\n alias: 'Umb.Condition.WorkspaceAlias',\r\n match: workspaceAlias\r\n },\r\n ],\r\n },\r\n {\r\n type: 'workspaceView',\r\n alias: 'Blueprint.WorkspaceView.Counter',\r\n name: 'Blueprint Counter View',\r\n element: () => import('./views/defaultWorkspace.element.js'),\r\n weight: 100,\r\n meta: {\r\n icon: 'icon-calculator',\r\n pathname: 'counter',\r\n label: 'Counter'\r\n },\r\n conditions: [\r\n {\r\n alias: 'Umb.Condition.WorkspaceAlias',\r\n match: workspaceAlias\r\n },\r\n ],\r\n },\r\n {\r\n type: 'workspaceContext',\r\n name: 'Blueprint Counter Workspace Context',\r\n alias: 'Blueprint.WorkspaceContext.Counter',\r\n api: () => import('./context.js'),\r\n conditions: [\r\n {\r\n alias: UMB_WORKSPACE_CONDITION_ALIAS,\r\n match: workspaceAlias,\r\n },\r\n ]\r\n }\r\n];\r\n","import { manifests as entrypoints } from \"./entrypoints/manifest.js\";\r\nimport { manifests as dashboards } from \"./dashboards/manifest.js\";\r\nimport { manifests as sections } from \"./sections/manifest.js\";\r\nimport { manifests as workspaces } from \"./workspaces/manifest.js\";\r\n\r\n// Job of the bundle is to collate all the manifests from different parts of the extension and load other manifests\r\n// We load this bundle from umbraco-package.json\r\nexport const manifests: Array<UmbExtensionManifest> = [\r\n ...entrypoints,\r\n ...dashboards,\r\n ...sections,\r\n ...workspaces,\r\n];\r\n"],"names":["manifests","sectionAlias","menuManifest","menuItemManifest","sectionSidebarAppManifest","workspaceAlias","UMB_WORKSPACE_CONDITION_ALIAS","entrypoints","dashboards","sections","workspaces"],"mappings":";AAAO,MAAMA,IAAyC;AAAA,EACpD;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,IACN,IAAI,MAAM,OAAO,0BAAiB;AAAA,EAAA;AAEtC,GCPaA,IAAyC;AAAA,EACpD;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,IACN,IAAI,MAAM,OAAO,iCAAwB;AAAA,IACzC,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,UAAU;AAAA,IAAA;AAAA,IAEZ,YAAY;AAAA,MACV;AAAA;AAAA,QAEE,OAAO;AAAA,QACP,OAAO;AAAA,MAAA;AAAA,IACT;AAAA,EACF;AAEJ,GCfMC,IAAe,qBAGfC,IAA6B;AAAA,EACjC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,OAAO;AAAA,EAAA;AAEX,GAGMC,IAAqC;AAAA,EACzC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,OAAO;AAAA,IACP,MAAM;AAAA,IACN,YAAY;AAAA;AAAA,IACZ,OAAO,CAAC,gBAAgB;AAAA,EAAA;AAE5B,GAGMC,IAAuD;AAAA,EAC3D,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,OAAO;AAAA,IACP,MAAM;AAAA,EAAA;AAAA,EAER,YAAY;AAAA,IACV;AAAA,MACE,OAAO;AAAA,MACP,OAAOH;AAAA,IAAA;AAAA,EACT;AAEJ,GAEaD,IAAyC;AAAA;AAAA,EAEpD;AAAA,IACE,MAAM;AAAA,IACN,OAAOC;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,UAAU;AAAA,IAAA;AAAA,EACZ;AAAA,EAEFG;AAAA,EACAF;AAAA,EACAC;AACF,GC5DME,IAAiB,uBAEVL,IAAyC;AAAA,EACpD;AAAA,IACE,MAAM;AAAA,IACN,OAAOK;AAAA,IACP,MAAM;AAAA,IACN,SAAS,MAAM,OAAO,iCAAwB;AAAA,IAC9C,MAAM;AAAA;AAAA,MAEJ,YAAY;AAAA,IAAA;AAAA,EACd;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,IACN,SAAS,MAAM,OAAO,wCAAqC;AAAA,IAC3D,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO;AAAA,IAAA;AAAA,IAET,YAAY;AAAA,MACV;AAAA,QACE,OAAO;AAAA,QACP,OAAOA;AAAA,MAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,OAAO;AAAA,IACP,MAAM;AAAA,IACN,SAAS,MAAM,OAAO,wCAAqC;AAAA,IAC3D,QAAQ;AAAA,IACR,MAAM;AAAA,MACJ,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO;AAAA,IAAA;AAAA,IAET,YAAY;AAAA,MACV;AAAA,QACE,OAAO;AAAA,QACP,OAAOA;AAAA,MAAA;AAAA,IACT;AAAA,EACF;AAAA,EAEF;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK,MAAM,OAAO,uBAAc;AAAA,IAChC,YAAY;AAAA,MACV;AAAA,QACE,OAAOC;AAAA,QACP,OAAOD;AAAA,MAAA;AAAA,IACT;AAAA,EACF;AAEJ,GCxDaL,IAAyC;AAAA,EACpD,GAAGO;AAAAA,EACH,GAAGC;AAAAA,EACH,GAAGC;AAAAA,EACH,GAAGC;AACL;"}{
"id": "MyExtension",
"name": "MyExtension",
"version": "0.0.0",
"allowTelemetry": true,
"extensions": [
{
"name": "My Extension Bundle",
"alias": "MyExtension.Bundle",
"type": "bundle",
"js": "/App_Plugins/MyExtension/my-extension.js"
}
]
}import { html as a, css as c, customElement as i } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement as u } from "@umbraco-cms/backoffice/lit-element";
var m = Object.getOwnPropertyDescriptor, d = (o, s, p, l) => {
for (var e = l > 1 ? void 0 : l ? m(s, p) : s, t = o.length - 1, n; t >= 0; t--)
(n = o[t]) && (e = n(e) || e);
return e;
};
let r = class extends u {
render() {
return a`
<umb-workspace-editor headline="Blueprint" alias="Blueprint.Workspace" .enforceNoFooter=${!0}>
</umb-workspace-editor>
`;
}
};
r.styles = c`
:host {
display: block;
height: 100%;
}
`;
r = d([
i("blueprint-workspace")
], r);
const h = r;
export {
r as BlueprintWorkspaceElement,
h as default
};
//# sourceMappingURL=workspace.element-Cqpx0ovr.js.map
{"version":3,"file":"workspace.element-Cqpx0ovr.js","sources":["../../../Client/src/workspaces/workspace.element.ts"],"sourcesContent":["import { css, html, customElement } from \"@umbraco-cms/backoffice/external/lit\";\r\nimport { UmbLitElement } from \"@umbraco-cms/backoffice/lit-element\";\r\n\r\n@customElement(\"blueprint-workspace\")\r\nexport class BlueprintWorkspaceElement extends UmbLitElement {\r\n override render() {\r\n return html`\r\n <umb-workspace-editor headline=\"Blueprint\" alias=\"Blueprint.Workspace\" .enforceNoFooter=${true}>\r\n </umb-workspace-editor>\r\n `;\r\n }\r\n\r\n static override styles = css`\r\n :host {\r\n display: block;\r\n height: 100%;\r\n }\r\n `;\r\n}\r\n\r\nexport default BlueprintWorkspaceElement;\r\n\r\ndeclare global {\r\n interface HTMLElementTagNameMap {\r\n \"blueprint-workspace\": BlueprintWorkspaceElement;\r\n }\r\n}\r\n"],"names":["BlueprintWorkspaceElement","UmbLitElement","html","css","__decorateClass","customElement","BlueprintWorkspaceElement_default"],"mappings":";;;;;;;AAIO,IAAMA,IAAN,cAAwCC,EAAc;AAAA,EAClD,SAAS;AAChB,WAAOC;AAAA,kGACuF,EAAI;AAAA;AAAA;AAAA,EAGpG;AAQF;AAdaF,EAQK,SAASG;AAAA;AAAA;AAAA;AAAA;AAAA;AARdH,IAANI,EAAA;AAAA,EADNC,EAAc,qBAAqB;AAAA,GACvBL,CAAA;AAgBb,MAAAM,IAAeN;"}# Build output - generated by Vite
wwwroot/App_Plugins/NotesWiki/{
"name": "notes-wiki-client",
"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:44325/umbraco/swagger/noteswiki/swagger.json",
"test:e2e": "npx playwright test --config=tests/playwright.e2e.config.ts",
"test:e2e:headed": "npx playwright test --config=tests/playwright.e2e.config.ts --headed",
"test:e2e:ui": "npx playwright test --config=tests/playwright.e2e.config.ts --ui"
},
"devDependencies": {
"@hey-api/client-fetch": "^0.10.0",
"@hey-api/openapi-ts": "^0.85.0",
"@playwright/test": "^1.56.0",
"@umbraco-cms/backoffice": "^*",
"@umbraco/playwright-testhelpers": "^17.0.11",
"chalk": "^5.4.1",
"node-fetch": "^3.3.2",
"typescript": "^5.8.3",
"vite": "^6.3.4"
}
}
{
"id": "NotesWiki",
"name": "Notes Wiki",
"version": "0.0.0",
"allowTelemetry": true,
"extensions": [
{
"name": "Notes Wiki Bundle",
"alias": "NotesWiki.Bundle",
"type": "bundle",
"js": "/App_Plugins/NotesWiki/notes-wiki.js"
}
]
}
import fetch from "node-fetch";
import chalk from "chalk";
import { createClient, defaultPlugins } 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:44325/umbraco/swagger/noteswiki/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(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`
);
console.error(
`Please verify or change the URL in the ${chalk.yellow(
"package.json"
)} for the script ${chalk.yellow("generate-client")}`
);
return;
}
console.log(`OpenAPI spec fetched successfully`);
console.log(
`Calling ${chalk.yellow("hey-api")} to generate TypeScript client`
);
await createClient({
input: swaggerUrl,
output: "src/api",
plugins: [
...defaultPlugins,
{
name: "@hey-api/client-fetch",
bundle: true,
exportFromIndex: true,
throwOnError: true,
},
{
name: "@hey-api/typescript",
enums: "typescript",
},
{
name: "@hey-api/sdk",
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-client")}`
);
});
// 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 './client';
/**
* 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:44325',
throwOnError: true
}));'use strict';var j=async(t,r)=>{let e=typeof r=="function"?await r(t):r;if(e)return t.scheme==="bearer"?`Bearer ${e}`:t.scheme==="basic"?`Basic ${btoa(e)}`:e},z=(t,r,e)=>{typeof e=="string"||e instanceof Blob?t.append(r,e):t.append(r,JSON.stringify(e));},I=(t,r,e)=>{typeof e=="string"?t.append(r,e):t.append(r,JSON.stringify(e));},k={bodySerializer:t=>{let r=new FormData;return Object.entries(t).forEach(([e,i])=>{i!=null&&(Array.isArray(i)?i.forEach(a=>z(r,e,a)):z(r,e,i));}),r}},R={bodySerializer:t=>JSON.stringify(t,(r,e)=>typeof e=="bigint"?e.toString():e)},$={bodySerializer:t=>{let r=new URLSearchParams;return Object.entries(t).forEach(([e,i])=>{i!=null&&(Array.isArray(i)?i.forEach(a=>I(r,e,a)):I(r,e,i));}),r.toString()}},U=t=>{switch(t){case "label":return ".";case "matrix":return ";";case "simple":return ",";default:return "&"}},_=t=>{switch(t){case "form":return ",";case "pipeDelimited":return "|";case "spaceDelimited":return "%20";default:return ","}},D=t=>{switch(t){case "label":return ".";case "matrix":return ";";case "simple":return ",";default:return "&"}},O=({allowReserved:t,explode:r,name:e,style:i,value:a})=>{if(!r){let n=(t?a:a.map(l=>encodeURIComponent(l))).join(_(i));switch(i){case "label":return `.${n}`;case "matrix":return `;${e}=${n}`;case "simple":return n;default:return `${e}=${n}`}}let o=U(i),s=a.map(n=>i==="label"||i==="simple"?t?n:encodeURIComponent(n):y({allowReserved:t,name:e,value:n})).join(o);return i==="label"||i==="matrix"?o+s:s},y=({allowReserved:t,name:r,value:e})=>{if(e==null)return "";if(typeof e=="object")throw new Error("Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these.");return `${r}=${t?e:encodeURIComponent(e)}`},q=({allowReserved:t,explode:r,name:e,style:i,value:a})=>{if(a instanceof Date)return `${e}=${a.toISOString()}`;if(i!=="deepObject"&&!r){let n=[];Object.entries(a).forEach(([f,p])=>{n=[...n,f,t?p:encodeURIComponent(p)];});let l=n.join(",");switch(i){case "form":return `${e}=${l}`;case "label":return `.${l}`;case "matrix":return `;${e}=${l}`;default:return l}}let o=D(i),s=Object.entries(a).map(([n,l])=>y({allowReserved:t,name:i==="deepObject"?`${e}[${n}]`:n,value:l})).join(o);return i==="label"||i==="matrix"?o+s:s};var H=/\{[^{}]+\}/g,B=({path:t,url:r})=>{let e=r,i=r.match(H);if(i)for(let a of i){let o=false,s=a.substring(1,a.length-1),n="simple";s.endsWith("*")&&(o=true,s=s.substring(0,s.length-1)),s.startsWith(".")?(s=s.substring(1),n="label"):s.startsWith(";")&&(s=s.substring(1),n="matrix");let l=t[s];if(l==null)continue;if(Array.isArray(l)){e=e.replace(a,O({explode:o,name:s,style:n,value:l}));continue}if(typeof l=="object"){e=e.replace(a,q({explode:o,name:s,style:n,value:l}));continue}if(n==="matrix"){e=e.replace(a,`;${y({name:s,value:l})}`);continue}let f=encodeURIComponent(n==="label"?`.${l}`:l);e=e.replace(a,f);}return e},A=({allowReserved:t,array:r,object:e}={})=>a=>{let o=[];if(a&&typeof a=="object")for(let s in a){let n=a[s];if(n!=null)if(Array.isArray(n)){let l=O({allowReserved:t,explode:true,name:s,style:"form",value:n,...r});l&&o.push(l);}else if(typeof n=="object"){let l=q({allowReserved:t,explode:true,name:s,style:"deepObject",value:n,...e});l&&o.push(l);}else {let l=y({allowReserved:t,name:s,value:n});l&&o.push(l);}}return o.join("&")},P=t=>{if(!t)return "stream";let r=t.split(";")[0]?.trim();if(r){if(r.startsWith("application/json")||r.endsWith("+json"))return "json";if(r==="multipart/form-data")return "formData";if(["application/","audio/","image/","video/"].some(e=>r.startsWith(e)))return "blob";if(r.startsWith("text/"))return "text"}},E=async({security:t,...r})=>{for(let e of t){let i=await j(e,r.auth);if(!i)continue;let a=e.name??"Authorization";switch(e.in){case "query":r.query||(r.query={}),r.query[a]=i;break;case "cookie":r.headers.append("Cookie",`${a}=${i}`);break;case "header":default:r.headers.set(a,i);break}return}},S=t=>W({baseUrl:t.baseUrl,path:t.path,query:t.query,querySerializer:typeof t.querySerializer=="function"?t.querySerializer:A(t.querySerializer),url:t.url}),W=({baseUrl:t,path:r,query:e,querySerializer:i,url:a})=>{let o=a.startsWith("/")?a:`/${a}`,s=(t??"")+o;r&&(s=B({path:r,url:s}));let n=e?i(e):"";return n.startsWith("?")&&(n=n.substring(1)),n&&(s+=`?${n}`),s},x=(t,r)=>{let e={...t,...r};return e.baseUrl?.endsWith("/")&&(e.baseUrl=e.baseUrl.substring(0,e.baseUrl.length-1)),e.headers=C(t.headers,r.headers),e},C=(...t)=>{let r=new Headers;for(let e of t){if(!e||typeof e!="object")continue;let i=e instanceof Headers?e.entries():Object.entries(e);for(let[a,o]of i)if(o===null)r.delete(a);else if(Array.isArray(o))for(let s of o)r.append(a,s);else o!==void 0&&r.set(a,typeof o=="object"?JSON.stringify(o):o);}return r},h=class{_fns;constructor(){this._fns=[];}clear(){this._fns=[];}getInterceptorIndex(r){return typeof r=="number"?this._fns[r]?r:-1:this._fns.indexOf(r)}exists(r){let e=this.getInterceptorIndex(r);return !!this._fns[e]}eject(r){let e=this.getInterceptorIndex(r);this._fns[e]&&(this._fns[e]=null);}update(r,e){let i=this.getInterceptorIndex(r);return this._fns[i]?(this._fns[i]=e,r):false}use(r){return this._fns=[...this._fns,r],this._fns.length-1}},v=()=>({error:new h,request:new h,response:new h}),N=A({allowReserved:false,array:{explode:true,style:"form"},object:{explode:true,style:"deepObject"}}),Q={"Content-Type":"application/json"},w=(t={})=>({...R,headers:Q,parseAs:"auto",querySerializer:N,...t});var J=(t={})=>{let r=x(w(),t),e=()=>({...r}),i=s=>(r=x(r,s),e()),a=v(),o=async s=>{let n={...r,...s,fetch:s.fetch??r.fetch??globalThis.fetch,headers:C(r.headers,s.headers)};n.security&&await E({...n,security:n.security}),n.body&&n.bodySerializer&&(n.body=n.bodySerializer(n.body)),(n.body===void 0||n.body==="")&&n.headers.delete("Content-Type");let l=S(n),f={redirect:"follow",...n},p=new Request(l,f);for(let c of a.request._fns)c&&(p=await c(p,n));let T=n.fetch,u=await T(p);for(let c of a.response._fns)c&&(u=await c(u,p,n));let m={request:p,response:u};if(u.ok){if(u.status===204||u.headers.get("Content-Length")==="0")return {data:{},...m};let c=(n.parseAs==="auto"?P(u.headers.get("Content-Type")):n.parseAs)??"json";if(c==="stream")return {data:u.body,...m};let b=await u[c]();return c==="json"&&(n.responseValidator&&await n.responseValidator(b),n.responseTransformer&&(b=await n.responseTransformer(b))),{data:b,...m}}let g=await u.text();try{g=JSON.parse(g);}catch{}let d=g;for(let c of a.error._fns)c&&(d=await c(g,u,p,n));if(d=d||{},n.throwOnError)throw d;return {error:d,...m}};return {buildUrl:S,connect:s=>o({...s,method:"CONNECT"}),delete:s=>o({...s,method:"DELETE"}),get:s=>o({...s,method:"GET"}),getConfig:e,head:s=>o({...s,method:"HEAD"}),interceptors:a,options:s=>o({...s,method:"OPTIONS"}),patch:s=>o({...s,method:"PATCH"}),post:s=>o({...s,method:"POST"}),put:s=>o({...s,method:"PUT"}),request:o,setConfig:i,trace:s=>o({...s,method:"TRACE"})}};exports.createClient=J;exports.createConfig=w;exports.formDataBodySerializer=k;exports.jsonBodySerializer=R;exports.urlSearchParamsBodySerializer=$;//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
export * from './client.gen';
export * from './sdk.gen';Related skills
How it compares
Choose umbraco-backoffice over individual sub-skills first when you need the full extension map and blueprint routing before implementing a specific UI location.
FAQ
What does the umbraco-backoffice skill provide?
umbraco-backoffice is the backbone routing skill with a complete map of 57 extension types, working blueprints, and links to specialized Umbraco backoffice sub-skills by UI location.
How many Umbraco backoffice skills ship in the collection?
The Umbraco-CMS-Backoffice-Skills repository includes 58 backoffice extension skills plus additional testing skills, with umbraco-backoffice routing across the full extension map.
When should developers invoke umbraco-backoffice first?
Developers should invoke umbraco-backoffice when starting a new backoffice customization, understanding how extension types connect, or finding the correct sub-skill for a dashboard, tree, or workspace.