diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/sorter-dashboard.ts b/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/sorter-dashboard.ts index 36f96e5de2..daefba21f6 100644 --- a/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/sorter-dashboard.ts +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/sorter-dashboard.ts @@ -75,6 +75,7 @@ export class ExampleSorterDashboard extends UmbElementMixin(LitElement) { .outer-wrapper { display: flex; + gap: var(--uui-size-layout-1); } `, ]; diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/sorter-group.ts b/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/sorter-group.ts index 85688f727d..d1a0c7e35e 100644 --- a/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/sorter-group.ts +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-nested-containers/sorter-group.ts @@ -78,6 +78,9 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) { :host { display: block; width: 100%; + border: 1px dashed rgba(122, 122, 122, 0.25); + border-radius: calc(var(--uui-border-radius) * 2); + padding: var(--uui-size-space-1); } .sorter-placeholder { @@ -87,14 +90,6 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) { .sorter-container { min-height: 20px; } - - example-sorter-group { - display: block; - width: 100%; - border: 1px dashed rgba(122, 122, 122, 0.25); - border-radius: calc(var(--uui-border-radius) * 2); - padding: var(--uui-size-space-1); - } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-dashboard.ts b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-dashboard.ts index 7747317257..feb0979603 100644 --- a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-dashboard.ts +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-dashboard.ts @@ -57,6 +57,7 @@ export class ExampleSorterDashboard extends UmbElementMixin(LitElement) { .outer-wrapper { display: flex; + gap: var(--uui-size-layout-1); } `, ]; diff --git a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-group.ts b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-group.ts index 6975854446..da4b209172 100644 --- a/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-group.ts +++ b/src/Umbraco.Web.UI.Client/examples/sorter-with-two-containers/sorter-group.ts @@ -69,6 +69,9 @@ export class ExampleSorterGroup extends UmbElementMixin(LitElement) { :host { display: block; width: 100%; + border: 1px dashed rgba(122, 122, 122, 0.25); + border-radius: calc(var(--uui-border-radius) * 2); + padding: var(--uui-size-space-1); } .sorter-placeholder { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 30a36f3953..03bac9db89 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -45,7 +45,7 @@ "./tree": "./dist-cms/packages/core/tree/index.js", "./variant": "./dist-cms/packages/core/variant/index.js", "./workspace": "./dist-cms/packages/core/workspace/index.js", - "./events": "./dist-cms/packages/core/umb-events/index.js", + "./event": "./dist-cms/packages/core/event/index.js", "./repository": "./dist-cms/packages/core/repository/index.js", "./temporary-file": "./dist-cms/packages/core/temporary-file/index.js", "./block": "./dist-cms/packages/block/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index 1663dd3d26..88aa57b3ef 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -136,6 +136,7 @@ export class UmbAppElement extends UmbLitElement { // Instruct all requests to use the auth flow to get and use the access_token for all subsequent requests OpenAPI.TOKEN = () => this.#authContext!.getLatestToken(); OpenAPI.WITH_CREDENTIALS = true; + OpenAPI.CREDENTIALS = 'include'; } #redirect() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts index 66114e53fe..9e2141b81d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts @@ -34,7 +34,7 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement private _directRoute?: string; @state() - private _createButtonLabel = this.localize.term('content_createEmpty'); + private _createButtonLabel = this.localize.term('blockEditor_addBlock'); @property({ attribute: false }) public set config(config: UmbPropertyEditorConfigCollection | undefined) { @@ -52,7 +52,7 @@ export class UmbPropertyEditorUIBlockGridElement extends UmbLitElement implement if (customCreateButtonLabel) { this._createButtonLabel = customCreateButtonLabel; } else if (this._blocks.length === 1) { - this._createButtonLabel = `${this.localize.term('general_add')} ${this._blocks[0].label}`; + this._createButtonLabel = this.localize.term('blockEditor_addThis', [this._blocks[0].label]); } //const useInlineEditingAsDefault = config.getValueByAlias('useInlineEditingAsDefault'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts index e12ac2b161..1beec67023 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts @@ -1,8 +1,4 @@ import { UMB_BLOCK_WORKSPACE_MODAL } from '../../workspace/index.js'; -import { - DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, - type UmbDocumentTypeItemModel, -} from '@umbraco-cms/backoffice/document-type'; import type { UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue, @@ -10,30 +6,19 @@ import type { UmbBlockTypeWithGroupKey, } from '@umbraco-cms/backoffice/block'; import { css, html, customElement, state, repeat, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { groupBy } from '@umbraco-cms/backoffice/external/lodash'; import { UMB_MODAL_CONTEXT, UmbModalBaseElement, UmbModalRouteRegistrationController, } from '@umbraco-cms/backoffice/modal'; -import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository'; @customElement('umb-block-catalogue-modal') export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue > { - #itemManager = new UmbRepositoryItemsManager( - this, - DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, - (x) => x.unique, - ); - @state() - private _blocks: Array = []; - - @state() - private _blockGroups: Array = []; + private _groupedBlocks: Array<{ name?: string; blocks: Array }> = []; @state() _openClipboard?: boolean; @@ -60,19 +45,6 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< this._workspacePath = routeBuilder({}); }); }); - - this.observe(this.#itemManager.items, (items) => { - this._blocks = items.map((item) => { - const blockGroup = this._blocks.find((block) => block.contentElementTypeKey === item.unique)?.groupKey; - const block: UmbBlockTypeWithGroupKey = { - contentElementTypeKey: item.unique, - label: item.name, - icon: item.icon ?? undefined, - groupKey: blockGroup, - }; - return block; - }); - }); } connectedCallback() { @@ -80,10 +52,17 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< if (!this.data) return; this._openClipboard = this.data.openClipboard ?? false; - this._blocks = this.data.blocks ?? []; - this._blockGroups = this.data.blockGroups ?? []; - this.#itemManager.setUniques(this._blocks.map((x) => x.contentElementTypeKey)); + const blocks: Array = this.data.blocks ?? []; + const blockGroups: Array = this.data.blockGroups ?? []; + + const noGroupBlocks = blocks.filter((block) => !blockGroups.find((group) => group.key === block.groupKey)); + const grouped = blockGroups.map((group) => ({ + name: group.name ?? '', + blocks: blocks.filter((block) => block.groupKey === group.key), + })); + + this._groupedBlocks = [{ blocks: noGroupBlocks }, ...grouped]; } render() { @@ -107,17 +86,10 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< } #renderCreateEmpty() { - const blockArrays = groupBy(this._blocks, 'groupKey'); - - const mappedGroupsAndBlocks = Object.entries(blockArrays).map(([key, value]) => { - const group = this._blockGroups.find((group) => group.key === key); - return { name: group?.name, blocks: value }; - }); - return html` - ${mappedGroupsAndBlocks.map( + ${this._groupedBlocks.map( (group) => html` - ${group.name ? html`

${group.name}

` : nothing} + ${group.name ? html`

${group.name}

` : nothing}
${repeat( group.blocks, @@ -141,12 +113,18 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< #renderViews() { return html` - (this._openClipboard = false)}> - Create Empty + (this._openClipboard = false)}> + Create Empty - (this._openClipboard = true)}> - Clipboard + (this._openClipboard = true)}> + Clipboard diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.ts index 7fa2415579..21cbf444cd 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/auth.context.ts @@ -1,21 +1,25 @@ import { UmbAuthFlow } from './auth-flow.js'; import { UMB_AUTH_CONTEXT } from './auth.context.token.js'; +import type { UmbOpenApiConfiguration } from './models/openApiConfiguration.js'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import { UmbBaseController } from '@umbraco-cms/backoffice/class-api'; import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { OpenAPI } from '@umbraco-cms/backoffice/backend-api'; export class UmbAuthContext extends UmbBaseController { #isAuthorized = new UmbBooleanState(false); readonly isAuthorized = this.#isAuthorized.asObservable(); #isBypassed = false; - #backofficePath: string; - + #serverUrl; + #backofficePath; #authFlow; + #openApi = OpenAPI; constructor(host: UmbControllerHostElement, serverUrl: string, backofficePath: string, isBypassed: boolean) { super(host); this.#isBypassed = isBypassed; + this.#serverUrl = serverUrl; this.#backofficePath = backofficePath; this.#authFlow = new UmbAuthFlow(serverUrl, this.#getRedirectUrl()); @@ -65,7 +69,7 @@ export class UmbAuthContext extends UmbBaseController { * * NB! The user may experience being redirected to the login screen if the token is expired. * - * @example + * @example Using the latest token * ```js * const token = await authContext.getLatestToken(); * const result = await fetch('https://my-api.com', { headers: { Authorization: `Bearer ${token}` } }); @@ -94,6 +98,51 @@ export class UmbAuthContext extends UmbBaseController { return this.#authFlow.signOut(); } + /** + * Get the server url to the Management API. + * @memberof UmbAuthContext + * @example Using the server url + * ```js + * const serverUrl = authContext.getServerUrl(); + * OpenAPI.BASE = serverUrl; + * ``` + * @example + * ```js + * const serverUrl = authContext.getServerUrl(); + * const token = await authContext.getLatestToken(); + * const result = await fetch(`${serverUrl}/umbraco/management/api/v1/my-resource`, { headers: { Authorization: `Bearer ${token}` } }); + * ``` + * @returns The server url to the Management API + */ + getServerUrl() { + return this.#serverUrl; + } + + /** + * Get the default OpenAPI configuration, which is set up to communicate with the Management API. + * @remark This is useful if you want to communicate with your own resources generated by the [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen) library. + * @memberof UmbAuthContext + * + * @example Using the default OpenAPI configuration + * ```js + * const defaultOpenApi = authContext.getOpenApiConfiguration(); + * OpenAPI.BASE = defaultOpenApi.base; + * OpenAPI.WITH_CREDENTIALS = defaultOpenApi.withCredentials; + * OpenAPI.CREDENTIALS = defaultOpenApi.credentials; + * OpenAPI.TOKEN = defaultOpenApi.token; + * ``` + * @returns The default OpenAPI configuration + */ + getOpenApiConfiguration(): UmbOpenApiConfiguration { + return { + base: OpenAPI.BASE, + version: OpenAPI.VERSION, + withCredentials: OpenAPI.WITH_CREDENTIALS, + credentials: OpenAPI.CREDENTIALS, + token: () => this.getLatestToken(), + }; + } + #getRedirectUrl() { return `${window.location.origin}${this.#backofficePath}`; } diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts index e2633039d7..e6dc7d478d 100644 --- a/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/index.ts @@ -1,2 +1,3 @@ export * from './auth.context.js'; export * from './auth.context.token.js'; +export * from './models/openApiConfiguration.js'; diff --git a/src/Umbraco.Web.UI.Client/src/shared/auth/models/openApiConfiguration.ts b/src/Umbraco.Web.UI.Client/src/shared/auth/models/openApiConfiguration.ts new file mode 100644 index 0000000000..0bdc8ea97a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/shared/auth/models/openApiConfiguration.ts @@ -0,0 +1,32 @@ +/** + * Configuration for the OpenAPI (Umbraco) server. This is used to communicate with the Management API. + * This is useful if you want to configure your Fetch, Axios or other HTTP client to communicate with the Management API. + * If you use the recommended resource generator [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen) this can be used to configure the `OpenAPI` object. + */ +export interface UmbOpenApiConfiguration { + /** + * The base URL of the OpenAPI (Umbraco) server. + */ + readonly base: string; + + /** + * The configured version of the Management API to use. + */ + readonly version: string; + + /** + * The `withCredentials` option for the Fetch API. + */ + readonly withCredentials: boolean; + + /** + * The `credentials` option for the Fetch API. + */ + readonly credentials: 'include' | 'omit' | 'same-origin'; + + /** + * The token to use for the Authorization header. + * @returns A resolver for the token to use for the Authorization header. + */ + readonly token: () => Promise; +}