diff --git a/src/Umbraco.Web.UI.Client/index.html b/src/Umbraco.Web.UI.Client/index.html
index 94d127f213..94d6a2ed4d 100644
--- a/src/Umbraco.Web.UI.Client/index.html
+++ b/src/Umbraco.Web.UI.Client/index.html
@@ -1,4 +1,4 @@
-
+
@@ -7,6 +7,7 @@
Umbraco
+
diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
index a0b7abb0d2..13a928e69c 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts
@@ -1,4 +1,9 @@
-import { UMB_AUTH_CONTEXT, UMB_MODAL_APP_AUTH, type UmbUserLoginState } from '@umbraco-cms/backoffice/auth';
+import {
+ UMB_AUTH_CONTEXT,
+ UMB_MODAL_APP_AUTH,
+ UMB_STORAGE_REDIRECT_URL,
+ type UmbUserLoginState,
+} from '@umbraco-cms/backoffice/auth';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
@@ -65,6 +70,9 @@ export class UmbAppAuthController extends UmbControllerBase {
throw new Error('[Fatal] Auth context is not available');
}
+ // Save the current state
+ sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, window.location.href);
+
// Figure out which providers are available
const availableProviders = await firstValueFrom(this.#authContext.getAuthProviders(umbExtensionsRegistry));
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 a9f33d80b1..c689e1e66a 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
@@ -4,7 +4,7 @@ import { UmbAppContext } from './app.context.js';
import { UmbServerConnection } from './server-connection.js';
import { UmbAppAuthController } from './app-auth.controller.js';
import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
-import { UmbAuthContext } from '@umbraco-cms/backoffice/auth';
+import { UMB_STORAGE_REDIRECT_URL, UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -86,7 +86,14 @@ export class UmbAppElement extends UmbLitElement {
: this.localize.term('errors_externalLoginFailed');
this.observe(this.#authContext.authorizationSignal, () => {
- history.replaceState(null, '', '');
+ // Redirect to the saved state or root
+ let currentRoute = '';
+ const savedRoute = sessionStorage.getItem(UMB_STORAGE_REDIRECT_URL);
+ if (savedRoute) {
+ sessionStorage.removeItem(UMB_STORAGE_REDIRECT_URL);
+ currentRoute = savedRoute.endsWith('logout') ? currentRoute : savedRoute;
+ }
+ history.replaceState(null, '', currentRoute);
});
}
@@ -99,6 +106,11 @@ export class UmbAppElement extends UmbLitElement {
component: () => import('../upgrader/upgrader.element.js'),
guards: [this.#isAuthorizedGuard()],
},
+ {
+ path: 'preview',
+ component: () => import('../preview/preview.element.js'),
+ guards: [this.#isAuthorizedGuard()],
+ },
{
path: 'logout',
resolve: () => {
diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-logo.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-logo.element.ts
index 6df071c5e2..1a2c453d63 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-logo.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-logo.element.ts
@@ -44,6 +44,7 @@ export class UmbBackofficeHeaderLogoElement extends UmbLitElement {
UmbTextStyles,
css`
#logo {
+ display: var(--umb-header-logo-display, inline);
--uui-button-padding-top-factor: 1;
--uui-button-padding-bottom-factor: 0.5;
margin-right: var(--uui-size-space-2);
diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header.element.ts
index feb5ef7681..3270a1bb74 100644
--- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header.element.ts
@@ -20,7 +20,7 @@ export class UmbBackofficeHeaderElement extends UmbLitElement {
}
#appHeader {
- background-color: var(--uui-color-header-surface);
+ background-color: var(--umb-header-background-color, var(--uui-color-header-surface));
display: flex;
align-items: center;
justify-content: space-between;
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/manifests.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/manifests.ts
new file mode 100644
index 0000000000..70b6c62c48
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/manifests.ts
@@ -0,0 +1,32 @@
+import type { ManifestPreviewAppProvider } from '@umbraco-cms/backoffice/extension-registry';
+
+export const manifests: Array = [
+ {
+ type: 'previewApp',
+ alias: 'Umb.PreviewApps.Device',
+ name: 'Preview: Device Switcher',
+ element: () => import('./preview-device.element.js'),
+ weight: 400,
+ },
+ {
+ type: 'previewApp',
+ alias: 'Umb.PreviewApps.Culture',
+ name: 'Preview: Culture Switcher',
+ element: () => import('./preview-culture.element.js'),
+ weight: 300,
+ },
+ {
+ type: 'previewApp',
+ alias: 'Umb.PreviewApps.OpenWebsite',
+ name: 'Preview: Open Website Button',
+ element: () => import('./preview-open-website.element.js'),
+ weight: 200,
+ },
+ {
+ type: 'previewApp',
+ alias: 'Umb.PreviewApps.Exit',
+ name: 'Preview: Exit Button',
+ element: () => import('./preview-exit.element.js'),
+ weight: 100,
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-culture.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-culture.element.ts
new file mode 100644
index 0000000000..bfa6d14c39
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-culture.element.ts
@@ -0,0 +1,103 @@
+import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
+import { css, customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
+import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
+
+const elementName = 'umb-preview-culture';
+
+@customElement(elementName)
+export class UmbPreviewCultureElement extends UmbLitElement {
+ #languageRepository = new UmbLanguageCollectionRepository(this);
+
+ @state()
+ private _culture?: UmbLanguageDetailModel;
+
+ @state()
+ private _cultures: Array = [];
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.#getCultures();
+ }
+
+ async #getCultures() {
+ const { data: langauges } = await this.#languageRepository.requestCollection({ skip: 0, take: 100 });
+ this._cultures = langauges?.items ?? [];
+
+ const searchParams = new URLSearchParams(window.location.search);
+ const culture = searchParams.get('culture');
+
+ if (culture && culture !== this._culture?.unique) {
+ this._culture = this._cultures.find((c) => c.unique === culture);
+ }
+ }
+
+ async #onClick(culture: UmbLanguageDetailModel) {
+ if (this._culture === culture) return;
+ this._culture = culture;
+
+ const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
+ previewContext.updateIFrame({ culture: culture.unique });
+ }
+
+ render() {
+ if (this._cultures.length <= 1) return nothing;
+ return html`
+
+
+
+ ${this._culture?.name ?? this.localize.term('treeHeaders_languages')}
+
+
+
+
+ ${repeat(
+ this._cultures,
+ (item) => item.unique,
+ (item) => html`
+ this.#onClick(item)}>
+
+
+ `,
+ )}
+
+
+ `;
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ border-left: 1px solid var(--uui-color-header-contrast);
+ --uui-button-font-weight: 400;
+ --uui-button-padding-left-factor: 3;
+ --uui-button-padding-right-factor: 3;
+ }
+
+ uui-button > div {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ }
+
+ umb-popover-layout {
+ --uui-color-surface: var(--uui-color-header-surface);
+ --uui-color-border: var(--uui-color-header-surface);
+ color: var(--uui-color-header-contrast);
+ }
+ `,
+ ];
+}
+
+export { UmbPreviewCultureElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbPreviewCultureElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-device.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-device.element.ts
new file mode 100644
index 0000000000..73ceccf8d7
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-device.element.ts
@@ -0,0 +1,149 @@
+import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
+import { css, customElement, html, property, repeat } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+
+export interface UmbPreviewDevice {
+ alias: string;
+ label: string;
+ css: string;
+ icon: string;
+ dimensions: { height: string; width: string };
+}
+
+const elementName = 'umb-preview-device';
+
+@customElement(elementName)
+export class UmbPreviewDeviceElement extends UmbLitElement {
+ #devices: Array = [
+ {
+ alias: 'fullsize',
+ label: 'Fit browser',
+ css: 'fullsize',
+ icon: 'icon-application-window-alt',
+ dimensions: { height: '100%', width: '100%' },
+ },
+ {
+ alias: 'desktop',
+ label: 'Desktop',
+ css: 'desktop shadow',
+ icon: 'icon-display',
+ dimensions: { height: '1080px', width: '1920px' },
+ },
+ {
+ alias: 'laptop',
+ label: 'Laptop',
+ css: 'laptop shadow',
+ icon: 'icon-laptop',
+ dimensions: { height: '768px', width: '1366px' },
+ },
+ {
+ alias: 'ipad-portrait',
+ label: 'Tablet portrait',
+ css: 'ipad-portrait shadow',
+ icon: 'icon-ipad',
+ dimensions: { height: '929px', width: '769px' },
+ },
+ {
+ alias: 'ipad-landscape',
+ label: 'Tablet landscape',
+ css: 'ipad-landscape shadow flip',
+ icon: 'icon-ipad',
+ dimensions: { height: '675px', width: '1024px' },
+ },
+ {
+ alias: 'smartphone-portrait',
+ label: 'Smartphone portrait',
+ css: 'smartphone-portrait shadow',
+ icon: 'icon-iphone',
+ dimensions: { height: '640px', width: '360px' },
+ },
+ {
+ alias: 'smartphone-landscape',
+ label: 'Smartphone landscape',
+ css: 'smartphone-landscape shadow flip',
+ icon: 'icon-iphone',
+ dimensions: { height: '360px', width: '640px' },
+ },
+ ];
+
+ @property({ attribute: false, type: Object })
+ device = this.#devices[0];
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.#changeDevice(this.device);
+ }
+
+ async #changeDevice(device: UmbPreviewDevice) {
+ if (device === this.device) return;
+
+ this.device = device;
+
+ const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
+
+ previewContext?.updateIFrame({
+ className: device.css,
+ height: device.dimensions.height,
+ width: device.dimensions.width,
+ });
+ }
+
+ render() {
+ return html`
+
+
+
+ ${this.device.label}
+
+
+
+
+ ${repeat(
+ this.#devices,
+ (item) => item.alias,
+ (item) => html`
+ this.#changeDevice(item)}>
+
+
+ `,
+ )}
+
+
+ `;
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ border-left: 1px solid var(--uui-color-header-contrast);
+ --uui-button-font-weight: 400;
+ --uui-button-padding-left-factor: 3;
+ --uui-button-padding-right-factor: 3;
+ }
+
+ uui-button > div {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ }
+
+ umb-popover-layout {
+ --uui-color-surface: var(--uui-color-header-surface);
+ --uui-color-border: var(--uui-color-header-surface);
+ color: var(--uui-color-header-contrast);
+ }
+ `,
+ ];
+}
+
+export { UmbPreviewDeviceElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbPreviewDeviceElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts
new file mode 100644
index 0000000000..001f86bbf8
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-exit.element.ts
@@ -0,0 +1,49 @@
+import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
+import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+
+const elementName = 'umb-preview-exit';
+@customElement(elementName)
+export class UmbPreviewExitElement extends UmbLitElement {
+ async #onClick() {
+ const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
+ previewContext.exitPreview(0);
+ }
+
+ render() {
+ return html`
+
+
+
+ ${this.localize.term('preview_endLabel')}
+
+
+ `;
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ border-left: 1px solid var(--uui-color-header-contrast);
+ --uui-button-font-weight: 400;
+ --uui-button-padding-left-factor: 3;
+ --uui-button-padding-right-factor: 3;
+ }
+
+ uui-button > div {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ }
+ `,
+ ];
+}
+
+export { UmbPreviewExitElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbPreviewExitElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts
new file mode 100644
index 0000000000..e1d9bc285f
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-open-website.element.ts
@@ -0,0 +1,49 @@
+import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
+import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+
+const elementName = 'umb-preview-open-website';
+@customElement(elementName)
+export class UmbPreviewOpenWebsiteElement extends UmbLitElement {
+ async #onClick() {
+ const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
+ previewContext.openWebsite();
+ }
+
+ render() {
+ return html`
+
+
+
+ ${this.localize.term('preview_openWebsiteLabel')}
+
+
+ `;
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ border-left: 1px solid var(--uui-color-header-contrast);
+ --uui-button-font-weight: 400;
+ --uui-button-padding-left-factor: 3;
+ --uui-button-padding-right-factor: 3;
+ }
+
+ uui-button > div {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ }
+ `,
+ ];
+}
+
+export { UmbPreviewOpenWebsiteElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbPreviewOpenWebsiteElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts
new file mode 100644
index 0000000000..eba05b4198
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts
@@ -0,0 +1,208 @@
+import { UMB_APP_CONTEXT } from '../app/app.context.js';
+import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
+import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
+import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
+import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
+import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+
+const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions';
+
+export class UmbPreviewContext extends UmbContextBase {
+ #culture?: string | null;
+ #serverUrl: string = '';
+ #webSocket?: WebSocket;
+ #unique?: string | null;
+
+ #iframeReady = new UmbBooleanState(false);
+ public readonly iframeReady = this.#iframeReady.asObservable();
+
+ #previewUrl = new UmbStringState(undefined);
+ public readonly previewUrl = this.#previewUrl.asObservable();
+
+ #documentPreviewRepository = new UmbDocumentPreviewRepository(this);
+
+ constructor(host: UmbControllerHost) {
+ super(host, UMB_PREVIEW_CONTEXT);
+ this.#init();
+ }
+
+ async #init() {
+ const appContext = await this.getContext(UMB_APP_CONTEXT);
+ this.#serverUrl = appContext.getServerUrl();
+
+ const params = new URLSearchParams(window.location.search);
+
+ this.#culture = params.get('culture');
+ this.#unique = params.get('id');
+
+ if (!this.#unique) {
+ console.error('No unique ID found in query string.');
+ return;
+ }
+
+ this.#setPreviewUrl();
+ }
+
+ #configureWebSocket() {
+ if (this.#webSocket && this.#webSocket.readyState < 2) return;
+
+ const url = `${this.#serverUrl.replace('https://', 'wss://')}/umbraco/PreviewHub`;
+
+ this.#webSocket = new WebSocket(url);
+
+ this.#webSocket.addEventListener('open', () => {
+ // NOTE: SignalR protocol handshake; it requires a terminating control character.
+ const endChar = String.fromCharCode(30);
+ this.#webSocket?.send(`{"protocol":"json","version":1}${endChar}`);
+ });
+
+ this.#webSocket.addEventListener('message', (event: MessageEvent) => {
+ if (!event?.data) return;
+
+ // NOTE: Strip the terminating control character, (from SignalR).
+ const data = event.data.substring(0, event.data.length - 1);
+ const json = JSON.parse(data) as { type: number; target: string; arguments: Array };
+
+ if (json.type === 1 && json.target === 'refreshed') {
+ const pageId = json.arguments?.[0];
+ if (pageId === this.#unique) {
+ this.#setPreviewUrl({ rnd: Math.random() });
+ }
+ }
+ });
+ }
+
+ #getSessionCount(): number {
+ return Math.max(Number(localStorage.getItem(UMB_LOCALSTORAGE_SESSION_KEY)), 0) || 0;
+ }
+
+ #setPreviewUrl(args?: { serverUrl?: string; unique?: string | null; culture?: string | null; rnd?: number }) {
+ const host = args?.serverUrl || this.#serverUrl;
+ const path = args?.unique || this.#unique;
+ const params = new URLSearchParams();
+ const culture = args?.culture || this.#culture;
+
+ if (culture) params.set('culture', culture);
+ if (args?.rnd) params.set('rnd', args.rnd.toString());
+
+ this.#previewUrl.setValue(`${host}/${path}?${params}`);
+ }
+
+ #setSessionCount(sessions: number) {
+ localStorage.setItem(UMB_LOCALSTORAGE_SESSION_KEY, sessions.toString());
+ }
+
+ checkSession() {
+ const sessions = this.#getSessionCount();
+ if (sessions > 0) return;
+
+ umbConfirmModal(this._host, {
+ headline: `Preview website?`,
+ content: `You have ended preview mode, do you want to enable it again to view the latest saved version of your website?`,
+ cancelLabel: 'View published version',
+ confirmLabel: 'Preview latest version',
+ })
+ .then(() => {
+ this.restartSession();
+ })
+ .catch(() => {
+ this.exitSession();
+ });
+ }
+
+ async exitPreview(sessions: number = 0) {
+ this.#setSessionCount(sessions);
+
+ // We are good to end preview mode.
+ if (sessions <= 0) {
+ await this.#documentPreviewRepository.exit();
+ }
+
+ if (this.#webSocket) {
+ this.#webSocket.close();
+ this.#webSocket = undefined;
+ }
+
+ const url = this.#previewUrl.getValue() as string;
+ window.location.replace(url);
+ }
+
+ async exitSession() {
+ let sessions = this.#getSessionCount();
+ sessions--;
+ this.exitPreview(sessions);
+ }
+
+ iframeLoaded(iframe: HTMLIFrameElement) {
+ if (!iframe) return;
+ this.#configureWebSocket();
+ this.#iframeReady.setValue(true);
+ }
+
+ getIFrameWrapper(): HTMLElement | undefined {
+ return this.getHostElement().shadowRoot?.querySelector('#wrapper') as HTMLElement;
+ }
+
+ openWebsite() {
+ const url = this.#previewUrl.getValue() as string;
+ window.open(url, '_blank');
+ }
+
+ // TODO: [LK] Figure out how to make `iframe.contentDocument` works, as it's not from SameOrigin.
+ reloadIFrame(iframe: HTMLIFrameElement) {
+ const document = iframe.contentDocument;
+ if (!document) return;
+
+ document.location.reload();
+ }
+
+ async restartSession() {
+ await this.#documentPreviewRepository.enter();
+ this.startSession();
+ }
+
+ startSession() {
+ let sessions = this.#getSessionCount();
+ sessions++;
+ this.#setSessionCount(sessions);
+ }
+
+ async updateIFrame(args?: { culture?: string; className?: string; height?: string; width?: string }) {
+ if (!args) return;
+
+ const wrapper = this.getIFrameWrapper();
+ if (!wrapper) return;
+
+ const scaleIFrame = () => {
+ if (wrapper.className === 'fullsize') {
+ wrapper.style.transform = '';
+ } else {
+ const wScale = document.body.offsetWidth / (wrapper.offsetWidth + 30);
+ const hScale = document.body.offsetHeight / (wrapper.offsetHeight + 30);
+ const scale = Math.min(wScale, hScale, 1); // get the lowest ratio, but not higher than 1
+ wrapper.style.transform = `scale(${scale})`;
+ }
+ };
+
+ window.addEventListener('resize', scaleIFrame);
+ wrapper.addEventListener('transitionend', scaleIFrame);
+
+ if (args.culture) {
+ this.#iframeReady.setValue(false);
+
+ const params = new URLSearchParams(window.location.search);
+ params.set('culture', args.culture);
+ const newRelativePathQuery = window.location.pathname + '?' + params.toString();
+ history.pushState(null, '', newRelativePathQuery);
+
+ this.#setPreviewUrl({ culture: args.culture });
+ }
+
+ if (args.className) wrapper.className = args.className;
+ if (args.height) wrapper.style.height = args.height;
+ if (args.width) wrapper.style.width = args.width;
+ }
+}
+
+export const UMB_PREVIEW_CONTEXT = new UmbContextToken('UmbPreviewContext');
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts
new file mode 100644
index 0000000000..35a8619227
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.element.ts
@@ -0,0 +1,204 @@
+import { manifests as previewApps } from './apps/manifests.js';
+import { UmbPreviewContext } from './preview.context.js';
+import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+
+const elementName = 'umb-preview';
+
+/**
+ * @element umb-preview
+ */
+@customElement(elementName)
+export class UmbPreviewElement extends UmbLitElement {
+ #context = new UmbPreviewContext(this);
+
+ constructor() {
+ super();
+
+ if (previewApps?.length) {
+ umbExtensionsRegistry.registerMany(previewApps);
+ }
+
+ this.observe(this.#context.iframeReady, (iframeReady) => (this._iframeReady = iframeReady));
+ this.observe(this.#context.previewUrl, (previewUrl) => (this._previewUrl = previewUrl));
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.addEventListener('visibilitychange', this.#onVisibilityChange);
+ window.addEventListener('beforeunload', () => this.#context.exitSession());
+ this.#context.startSession();
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListener('visibilitychange', this.#onVisibilityChange);
+ // NOTE: Unsure how we remove an anonymous function from 'beforeunload' event listener.
+ // The reason for the anonymous function is that if we used a named function,
+ // `this` would be the `window` and would not have context to the class instance. [LK]
+ //window.removeEventListener('beforeunload', () => this.#context.exitSession());
+ this.#context.exitSession();
+ }
+
+ @state()
+ private _iframeReady?: boolean;
+
+ @state()
+ private _previewUrl?: string;
+
+ #onIFrameLoad(event: Event & { target: HTMLIFrameElement }) {
+ this.#context.iframeLoaded(event.target);
+ }
+
+ #onVisibilityChange() {
+ this.#context.checkSession();
+ }
+
+ render() {
+ if (!this._previewUrl) return nothing;
+ return html`
+ ${when(!this._iframeReady, () => html`
`)}
+
+
+ `;
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ padding-bottom: 40px;
+ }
+
+ #loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ font-size: 6rem;
+ backdrop-filter: blur(5px);
+ }
+
+ #wrapper {
+ transition: all 240ms cubic-bezier(0.165, 0.84, 0.44, 1);
+ flex-shrink: 0;
+ height: 100%;
+ width: 100%;
+ }
+
+ #wrapper.fullsize {
+ margin: 0 auto;
+ overflow: hidden;
+ }
+
+ #wrapper.shadow {
+ margin: 10px auto;
+ background-color: white;
+ border-radius: 3px;
+ overflow: hidden;
+ opacity: 1;
+ box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.26);
+ }
+
+ #container {
+ width: 100%;
+ height: 100%;
+ margin: 0 auto;
+ overflow: hidden;
+ }
+
+ #menu {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ background-color: var(--uui-color-header-surface);
+ height: 40px;
+
+ animation: menu-bar-animation 1.2s;
+ animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
+ }
+
+ #menu > h4 {
+ color: var(--uui-color-header-contrast-emphasis);
+ margin: 0;
+ padding: 0 15px;
+ }
+
+ #menu > uui-button-group {
+ height: 100%;
+ }
+
+ uui-icon.flip {
+ rotate: 90deg;
+ }
+
+ iframe {
+ border: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ }
+
+ @keyframes menu-bar-animation {
+ 0% {
+ bottom: -50px;
+ }
+ 40% {
+ bottom: -50px;
+ }
+ 80% {
+ bottom: 0px;
+ }
+ }
+ `,
+ ];
+}
+
+export default UmbPreviewElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbPreviewElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.stories.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.stories.ts
new file mode 100644
index 0000000000..fb6732e13d
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.stories.ts
@@ -0,0 +1,10 @@
+import type { Meta } from '@storybook/web-components';
+import { html } from '@umbraco-cms/backoffice/external/lit';
+
+export default {
+ title: 'Apps/Preview',
+ component: 'umb-preview',
+ id: 'umb-preview',
+} satisfies Meta;
+
+export const Preview = () => html``;
diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.test.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.test.ts
new file mode 100644
index 0000000000..3037fbf6da
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.test.ts
@@ -0,0 +1,14 @@
+import { expect, fixture, html } from '@open-wc/testing';
+import { UmbPreviewElement } from './preview.element.js';
+
+describe('UmbPreview', () => {
+ let element: UmbPreviewElement;
+
+ beforeEach(async () => {
+ element = await fixture(html``);
+ });
+
+ it('is defined with its own instance', () => {
+ expect(element).to.be.instanceOf(UmbPreviewElement);
+ });
+});
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
index fa0a3caba6..5c068d7bc7 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts
@@ -548,13 +548,15 @@ export default {
noIconsFound: 'Ingen ikoner blev fundet',
noMacroParams: 'Der er ingen parametre for denne makro',
noMacros: 'Der er ikke tilføjet nogen makroer',
- externalLoginProviders: 'Eksterne login-udbydere',
+ externalLoginProviders: 'Eksternt login',
exceptionDetail: 'Undtagelsesdetaljer',
stacktrace: 'Stacktrace',
innerException: 'Indre undtagelse',
- linkYour: 'Link dit',
- unLinkYour: 'Fjern link fra dit',
- account: 'konto',
+ linkYour: 'Link din {0} konto',
+ linkYourConfirm: 'For at linke dine Umbraco og {0} konti, vil du blive sendt til {0} for at bekræfte.',
+ unLinkYour: 'Fjern link fra din {0} konto',
+ unLinkYourConfirm: 'Du er ved at fjerne linket mellem dine Umbraco og {0} konti og du vil blive logget ud.',
+ linkedToService: 'Din konto er linket til denne service',
selectEditor: 'Vælg editor',
selectEditorConfiguration: 'Vælg konfiguration',
selectSnippet: 'Vælg snippet',
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
index a1f3c17630..0f840ec4cd 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts
@@ -559,13 +559,16 @@ export default {
noIconsFound: 'No icons were found',
noMacroParams: 'There are no parameters for this macro',
noMacros: 'There are no macros available to insert',
- externalLoginProviders: 'External login providers',
+ externalLoginProviders: 'External logins',
exceptionDetail: 'Exception Details',
stacktrace: 'Stacktrace',
innerException: 'Inner Exception',
- linkYour: 'Link your',
- unLinkYour: 'Un-link your',
- account: 'account',
+ linkYour: 'Link your {0} account',
+ linkYourConfirm:
+ 'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm.',
+ unLinkYour: 'Un-link your {0} account',
+ unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.',
+ linkedToService: 'Your account is linked to this service',
selectEditor: 'Select editor',
selectEditorConfiguration: 'Select configuration',
selectSnippet: 'Select snippet',
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
index af418baa1b..cfef63d647 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
@@ -568,13 +568,16 @@ export default {
noIconsFound: 'No icons were found',
noMacroParams: 'There are no parameters for this macro',
noMacros: 'There are no macros available to insert',
- externalLoginProviders: 'External login providers',
+ externalLoginProviders: 'External logins',
exceptionDetail: 'Exception Details',
stacktrace: 'Stacktrace',
innerException: 'Inner Exception',
- linkYour: 'Link your',
- unLinkYour: 'Un-link your',
- account: 'account',
+ linkYour: 'Link your {0} account',
+ linkYourConfirm:
+ 'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm.',
+ unLinkYour: 'Un-link your {0} account',
+ unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.',
+ linkedToService: 'Your account is linked to this service',
selectEditor: 'Select editor',
selectSnippet: 'Select snippet',
variantdeletewarning:
diff --git a/src/Umbraco.Web.UI.Client/src/css/user-defined.css b/src/Umbraco.Web.UI.Client/src/css/user-defined.css
new file mode 100644
index 0000000000..35c48a145b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/css/user-defined.css
@@ -0,0 +1 @@
+/* This file can be overridden by placing a file with the same name in the /wwwroot/umbraco/backoffice/css folder of the website */
diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts
index 18dc20cc0f..78495eea2c 100644
--- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts
+++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models.ts
@@ -1057,15 +1057,6 @@ fallbackIsoCode?: string | null
isoCode: string
};
-export type LinkedLoginModel = {
- providerName: string
-providerKey: string
- };
-
-export type LinkedLoginsRequestModel = {
- linkedLogins: Array
- };
-
export type LogLevelCountsReponseModel = {
information: number
debug: number
@@ -2585,6 +2576,7 @@ key: string
export type UserExternalLoginProviderModel = {
providerSchemeName: string
+providerKey?: string | null
isLinkedOnUser: boolean
hasManualLinkingEnabled: boolean
};
@@ -5241,7 +5233,6 @@ PostUserUnlock: {
,PostUserCurrentChangePassword: string
,GetUserCurrentConfiguration: CurrenUserConfigurationResponseModel
,GetUserCurrentLoginProviders: Array
- ,GetUserCurrentLogins: LinkedLoginsRequestModel
,GetUserCurrentPermissions: UserPermissionsResponseModel
,GetUserCurrentPermissionsDocument: Array
,GetUserCurrentPermissionsMedia: UserPermissionsResponseModel
diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts
index c634a46642..861e3352db 100644
--- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts
+++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services.ts
@@ -8701,21 +8701,6 @@ requestBody
});
}
- /**
- * @returns unknown Success
- * @throws ApiError
- */
- public static getUserCurrentLogins(): CancelablePromise {
-
- return __request(OpenAPI, {
- method: 'GET',
- url: '/umbraco/management/api/v1/user/current/logins',
- errors: {
- 401: `The resource is protected and requires an authentication token`,
- },
- });
- }
-
/**
* @returns unknown Success
* @throws ApiError
diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts
index 51ddde3466..0eaa8a1855 100644
--- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts
+++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts
@@ -831,7 +831,6 @@ export const data: Array = [
{ alias: 'icon', value: 'icon-layers' },
{ alias: 'tabName', value: 'Children' },
{ alias: 'showContentFirst', value: true },
- { alias: 'useInfiniteEditor', value: true },
],
},
{
@@ -876,7 +875,6 @@ export const data: Array = [
{ alias: 'icon', value: 'icon-layers' },
{ alias: 'tabName', value: 'Items' },
{ alias: 'showContentFirst', value: false },
- { alias: 'useInfiniteEditor', value: true },
],
},
{
diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts
index ddbd634a65..a1657a2531 100644
--- a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts
+++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts
@@ -128,8 +128,4 @@ export const mfaLoginProviders: Array = [
isEnabledOnUser: false,
providerName: 'sms',
},
- {
- isEnabledOnUser: true,
- providerName: 'Email',
- },
];
diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts
index 4806994c0d..b8a500f4bb 100644
--- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts
+++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/manifests.handlers.ts
@@ -76,6 +76,15 @@ const privateManifests: PackageManifestResponse = [
label: 'Setup SMS Verification',
},
},
+ {
+ type: 'mfaLoginProvider',
+ alias: 'My.MfaLoginProvider.Custom.Email',
+ name: 'My Custom Email MFA Provider',
+ forProviderName: 'email',
+ meta: {
+ label: 'Setup Email Verification',
+ },
+ },
],
},
{
@@ -92,20 +101,6 @@ const privateManifests: PackageManifestResponse = [
},
],
},
- {
- name: 'My MFA Package',
- extensions: [
- {
- type: 'mfaLoginProvider',
- alias: 'My.MfaLoginProvider.Custom',
- name: 'My Custom MFA Provider',
- forProviderName: 'sms',
- meta: {
- label: 'Setup SMS Verification',
- },
- },
- ],
- },
];
const publicManifests: PackageManifestResponse = [
diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts
index 9454d23c56..cff60866fd 100644
--- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts
+++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/current.handlers.ts
@@ -1,7 +1,7 @@
const { rest } = window.MockServiceWorker;
import { umbUserMockDb } from '../../data/user/user.db.js';
import { UMB_SLUG } from './slug.js';
-import type { LinkedLoginsRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
+import type { UserData } from '@umbraco-cms/backoffice/external/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
@@ -9,19 +9,22 @@ export const handlers = [
const loggedInUser = umbUserMockDb.getCurrentUser();
return res(ctx.status(200), ctx.json(loggedInUser));
}),
- rest.get(umbracoPath(`${UMB_SLUG}/current/logins`), (_req, res, ctx) => {
- return res(
- ctx.status(200),
- ctx.json({
- linkedLogins: [
+ rest.get(
+ umbracoPath(`${UMB_SLUG}/current/login-providers`),
+ (_req, res, ctx) => {
+ return res(
+ ctx.status(200),
+ ctx.json([
{
+ hasManualLinkingEnabled: true,
+ isLinkedOnUser: true,
providerKey: 'google',
- providerName: 'Umbraco.Google',
+ providerSchemeName: 'Umbraco.Google',
},
- ],
- }),
- );
- }),
+ ]),
+ );
+ },
+ ),
rest.get(umbracoPath(`${UMB_SLUG}/current/2fa`), (_req, res, ctx) => {
const mfaLoginProviders = umbUserMockDb.getMfaLoginProviders();
return res(ctx.status(200), ctx.json(mfaLoginProviders));
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
index 1c139bf595..b359ddf352 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts
@@ -98,6 +98,11 @@ export class UmbAuthFlow {
// tokens
#tokenResponse?: TokenResponse;
+ // external login
+ #link_endpoint;
+ #link_key_endpoint;
+ #unlink_endpoint;
+
/**
* This signal will emit when the authorization flow is complete.
* @remark It will also emit if there is an error during the authorization flow.
@@ -125,6 +130,10 @@ export class UmbAuthFlow {
end_session_endpoint: `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/signout`,
});
+ this.#link_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/link-login`;
+ this.#link_key_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/link-login-key`;
+ this.#unlink_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/unlink-login`;
+
this.#notifier = new AuthorizationNotifier();
this.#tokenHandler = new BaseTokenRequestHandler(requestor);
this.#storageBackend = new LocalStorageBackend();
@@ -320,6 +329,55 @@ export class UmbAuthFlow {
: Promise.reject('Missing tokenResponse.');
}
+ /**
+ * This method will link the current user to the specified provider by redirecting the user to the link endpoint.
+ * @param provider The provider to link to.
+ */
+ async linkLogin(provider: string): Promise {
+ const linkKey = await this.#makeLinkTokenRequest(provider);
+
+ const form = document.createElement('form');
+ form.method = 'POST';
+ form.action = this.#link_endpoint;
+ form.style.display = 'none';
+
+ const providerInput = document.createElement('input');
+ providerInput.name = 'provider';
+ providerInput.value = provider;
+ form.appendChild(providerInput);
+
+ const linkKeyInput = document.createElement('input');
+ linkKeyInput.name = 'linkKey';
+ linkKeyInput.value = linkKey;
+ form.appendChild(linkKeyInput);
+
+ document.body.appendChild(form);
+ form.submit();
+ }
+
+ /**
+ * This method will unlink the current user from the specified provider.
+ */
+ async unlinkLogin(loginProvider: string, providerKey: string): Promise {
+ const token = await this.performWithFreshTokens();
+ const request = new Request(this.#unlink_endpoint, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
+ body: JSON.stringify({ loginProvider, providerKey }),
+ });
+
+ const result = await fetch(request);
+
+ if (!result.ok) {
+ const error = await result.json();
+ throw error;
+ }
+
+ await this.signOut();
+
+ return true;
+ }
+
/**
* Save the current token response to local storage.
*/
@@ -384,4 +442,21 @@ export class UmbAuthFlow {
return false;
}
}
+
+ async #makeLinkTokenRequest(provider: string) {
+ const token = await this.performWithFreshTokens();
+
+ const request = await fetch(`${this.#link_key_endpoint}?provider=${provider}`, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!request.ok) {
+ throw new Error('Failed to link login');
+ }
+
+ return request.json();
+ }
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
index f4a418727d..852707b59a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
@@ -12,12 +12,8 @@ export class UmbAuthContext extends UmbContextBase {
#isAuthorized = new UmbBooleanState(false);
// Timeout is different from `isAuthorized` because it can occur repeatedly
#isTimeout = new Subject();
- /**
- * Observable that emits true when the auth context is initialized.
- * @remark It will only emit once and then complete itself.
- */
#isInitialized = new ReplaySubject(1);
- #isBypassed = false;
+ #isBypassed;
#serverUrl;
#backofficePath;
#authFlow;
@@ -25,6 +21,12 @@ export class UmbAuthContext extends UmbContextBase {
#authWindowProxy?: WindowProxy | null;
#previousAuthUrl?: string;
+ /**
+ * Observable that emits true when the auth context is initialized.
+ * @remark It will only emit once and then complete itself.
+ */
+ readonly isInitialized = this.#isInitialized.asObservable();
+
/**
* Observable that emits true if the user is authorized, otherwise false.
* @remark It will only emit when the authorization state changes.
@@ -254,22 +256,51 @@ export class UmbAuthContext extends UmbContextBase {
};
}
+ /**
+ * Sets the auth context as initialized, which means that the auth context is ready to be used.
+ * @remark This is used to let the app context know that the core module is ready, which means that the core auth providers are available.
+ */
setInitialized() {
this.#isInitialized.next();
this.#isInitialized.complete();
}
+ /**
+ * Gets all registered auth providers.
+ */
getAuthProviders(extensionsRegistry: UmbBackofficeExtensionRegistry) {
return this.#isInitialized.pipe(
switchMap(() => extensionsRegistry.byType<'authProvider', ManifestAuthProvider>('authProvider')),
);
}
+ /**
+ * Gets the authorized redirect url.
+ * @returns The redirect url, which is the backoffice path.
+ */
getRedirectUrl() {
return `${window.location.origin}${this.#backofficePath}${this.#backofficePath.endsWith('/') ? '' : '/'}oauth_complete`;
}
+ /**
+ * Gets the post logout redirect url.
+ * @returns The post logout redirect url, which is the backoffice path with the logout path appended.
+ */
getPostLogoutRedirectUrl() {
return `${window.location.origin}${this.#backofficePath}${this.#backofficePath.endsWith('/') ? '' : '/'}logout`;
}
+
+ /**
+ * @see UmbAuthFlow#linkLogin
+ */
+ linkLogin(provider: string) {
+ return this.#authFlow.linkLogin(provider);
+ }
+
+ /**
+ * @see UmbAuthFlow#unlinkLogin
+ */
+ unlinkLogin(providerName: string, providerKey: string) {
+ return this.#authFlow.unlinkLogin(providerName, providerKey);
+ }
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts
index eaa9cfe801..1a7bdec3fb 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/components/auth-provider-default.element.ts
@@ -18,19 +18,25 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA
this.setAttribute('part', 'auth-provider-default');
}
+ get #label() {
+ const label = this.manifest.meta?.label ?? this.manifest.forProviderName;
+ const labelLocalized = this.localize.string(label);
+ return this.localize.term('login_signInWith', labelLocalized);
+ }
+
render() {
return html`
this.onSubmit(this.manifest)}
id="auth-provider-button"
- .label=${this.manifest.meta?.label ?? this.manifest.forProviderName}
+ .label=${this.#label}
.look=${this.manifest.meta?.defaultView?.look ?? 'outline'}
.color=${this.manifest.meta?.defaultView?.color ?? 'default'}>
${this.manifest.meta?.defaultView?.icon
- ? html``
+ ? html``
: nothing}
- ${this.manifest.meta?.label ?? this.manifest.forProviderName}
+ ${this.#label}
`;
}
@@ -45,6 +51,10 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA
#auth-provider-button {
width: 100%;
}
+
+ #icon {
+ margin-right: var(--uui-size-space-1);
+ }
`,
];
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/providers/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/providers/manifests.ts
index 5c8e7dc2de..bab8a1ac83 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/providers/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/providers/manifests.ts
@@ -8,7 +8,7 @@ export const manifests: Array = [
forProviderName: 'Umbraco',
weight: 1000,
meta: {
- label: 'Sign in with Umbraco',
+ label: 'Umbraco',
defaultView: {
icon: 'icon-umbraco',
look: 'primary',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-alias.condition.ts
index 55c24bca7b..166d81e131 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-alias.condition.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-alias.condition.ts
@@ -1,4 +1,4 @@
-import { UMB_COLLECTION_CONTEXT } from './default/collection-default.context.js';
+import { UMB_COLLECTION_CONTEXT } from './default/index.js';
import type { CollectionAliasConditionConfig } from './collection-alias.manifest.js';
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-bulk-action-permission.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-bulk-action-permission.condition.ts
index b96ab7e9b7..3b13ad47e2 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-bulk-action-permission.condition.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-bulk-action-permission.condition.ts
@@ -1,4 +1,4 @@
-import { UMB_COLLECTION_CONTEXT } from './default/collection-default.context.js';
+import { UMB_COLLECTION_CONTEXT } from './default/index.js';
import type { CollectionBulkActionPermissionConditionConfig } from './collection-bulk-action-permission.manifest.js';
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-selection-actions.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-selection-actions.element.ts
index 4b5cb7ebff..eec5e5782b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-selection-actions.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-selection-actions.element.ts
@@ -1,9 +1,9 @@
-import { UMB_COLLECTION_CONTEXT } from '../default/collection-default.context.js';
-import type { ManifestEntityBulkAction, MetaEntityBulkAction } from '../../extension-registry/models/index.js';
+import { UMB_COLLECTION_CONTEXT } from '../default/index.js';
import type { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import type { ManifestEntityBulkAction, MetaEntityBulkAction } from '@umbraco-cms/backoffice/extension-registry';
function apiArgsMethod(manifest: ManifestEntityBulkAction) {
return [{ meta: manifest.meta }] as unknown[];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts
index fa6fbf6d63..72f015e95b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/collection-view-bundle.element.ts
@@ -1,5 +1,5 @@
-import type { UmbDefaultCollectionContext } from '../default/collection-default.context.js';
-import { UMB_COLLECTION_CONTEXT } from '../default/collection-default.context.js';
+import type { UmbDefaultCollectionContext } from '../default/index.js';
+import { UMB_COLLECTION_CONTEXT } from '../default/index.js';
import type { UmbCollectionLayoutConfiguration } from '../types.js';
import { css, html, customElement, state, nothing, repeat, query } from '@umbraco-cms/backoffice/external/lit';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/pagination/collection-pagination.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/pagination/collection-pagination.element.ts
index f2df8ee11b..af6f916797 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/pagination/collection-pagination.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/components/pagination/collection-pagination.element.ts
@@ -1,4 +1,4 @@
-import { UMB_COLLECTION_CONTEXT } from '../../default/collection-default.context.js';
+import { UMB_COLLECTION_CONTEXT } from '../../default/index.js';
import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, nothing, state } from '@umbraco-cms/backoffice/external/lit';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context-token.ts
new file mode 100644
index 0000000000..19a71b3408
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context-token.ts
@@ -0,0 +1,4 @@
+import type { UmbDefaultCollectionContext } from './collection-default.context.js';
+import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
+
+export const UMB_COLLECTION_CONTEXT = new UmbContextToken('UmbCollectionContext');
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts
index 76f087c353..542304592c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.context.ts
@@ -6,23 +6,31 @@ import type {
UmbCollectionContext,
UmbCollectionLayoutConfiguration,
} from '../types.js';
+import type { UmbCollectionFilterModel } from '../collection-filter-model.interface.js';
+import type { UmbCollectionRepository } from '../repository/collection-repository.interface.js';
+import { UMB_COLLECTION_CONTEXT } from './collection-default.context-token.js';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbArrayState, UmbNumberState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
-import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbSelectionManager, UmbPaginationManager } from '@umbraco-cms/backoffice/utils';
import type { ManifestCollection, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
-import type { UmbCollectionFilterModel, UmbCollectionRepository } from '@umbraco-cms/backoffice/collection';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import {
+ UmbRequestReloadChildrenOfEntityEvent,
+ UmbRequestReloadStructureForEntityEvent,
+} from '@umbraco-cms/backoffice/entity-action';
+import type { UmbActionEventContext } from '@umbraco-cms/backoffice/action';
+import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
+import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
const LOCAL_STORAGE_KEY = 'umb-collection-view';
export class UmbDefaultCollectionContext<
- CollectionItemType = any,
- FilterModelType extends UmbCollectionFilterModel = any,
+ CollectionItemType extends { entityType: string; unique: string } = any,
+ FilterModelType extends UmbCollectionFilterModel = UmbCollectionFilterModel,
>
extends UmbContextBase
implements UmbCollectionContext, UmbApi
@@ -34,7 +42,7 @@ export class UmbDefaultCollectionContext<
#loading = new UmbObjectState(false);
public readonly loading = this.#loading.asObservable();
- #items = new UmbArrayState([], (x) => x);
+ #items = new UmbArrayState([], (x) => x.unique);
public readonly items = this.#items.asObservable();
#totalItems = new UmbNumberState(0);
@@ -63,6 +71,8 @@ export class UmbDefaultCollectionContext<
this.#initialized ? resolve() : (this.#initResolver = resolve);
});
+ #actionEventContext: UmbActionEventContext | undefined;
+
constructor(host: UmbControllerHost, defaultViewAlias: string, defaultFilter: Partial = {}) {
super(host, UMB_COLLECTION_CONTEXT);
@@ -70,6 +80,33 @@ export class UmbDefaultCollectionContext<
this.#defaultFilter = defaultFilter;
this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange);
+ this.#listenToEntityEvents();
+ }
+
+ async #listenToEntityEvents() {
+ this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => {
+ this.#actionEventContext = context;
+
+ context?.removeEventListener(
+ UmbRequestReloadStructureForEntityEvent.TYPE,
+ this.#onReloadStructureRequest as unknown as EventListener,
+ );
+
+ context?.removeEventListener(
+ UmbRequestReloadChildrenOfEntityEvent.TYPE,
+ this.#onReloadChildrenRequest as unknown as EventListener,
+ );
+
+ context?.addEventListener(
+ UmbRequestReloadStructureForEntityEvent.TYPE,
+ this.#onReloadStructureRequest as unknown as EventListener,
+ );
+
+ context?.addEventListener(
+ UmbRequestReloadChildrenOfEntityEvent.TYPE,
+ this.#onReloadChildrenRequest as unknown as EventListener,
+ );
+ });
}
#configured = false;
@@ -222,11 +259,37 @@ export class UmbDefaultCollectionContext<
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(layouts));
}
+
+ #onReloadStructureRequest = (event: UmbRequestReloadStructureForEntityEvent) => {
+ const items = this.#items.getValue();
+ const hasItem = items.some((item) => item.unique === event.getUnique());
+ if (hasItem) {
+ this.requestCollection();
+ }
+ };
+
+ #onReloadChildrenRequest = async (event: UmbRequestReloadChildrenOfEntityEvent) => {
+ // check if the collection is in the same context as the entity from the event
+ const entityContext = await this.getContext(UMB_ENTITY_CONTEXT);
+ const unique = entityContext.getUnique();
+ const entityType = entityContext.getEntityType();
+
+ if (unique === event.getUnique() && entityType === event.getEntityType()) {
+ this.requestCollection();
+ }
+ };
+
+ destroy(): void {
+ this.#actionEventContext?.removeEventListener(
+ UmbRequestReloadStructureForEntityEvent.TYPE,
+ this.#onReloadStructureRequest as unknown as EventListener,
+ );
+
+ this.#actionEventContext?.removeEventListener(
+ UmbRequestReloadChildrenOfEntityEvent.TYPE,
+ this.#onReloadChildrenRequest as unknown as EventListener,
+ );
+
+ super.destroy();
+ }
}
-
-export const UMB_COLLECTION_CONTEXT = new UmbContextToken('UmbCollectionContext');
-
-/**
- * @deprecated Use UMB_COLLECTION_CONTEXT instead.
- */
-export { UMB_COLLECTION_CONTEXT as UMB_DEFAULT_COLLECTION_CONTEXT };
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts
index f57b0043b8..76c3002045 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/collection-default.element.ts
@@ -1,9 +1,9 @@
-import { UMB_COLLECTION_CONTEXT, UmbDefaultCollectionContext } from './collection-default.context.js';
+import { UmbDefaultCollectionContext } from './collection-default.context.js';
+import { UMB_COLLECTION_CONTEXT } from './collection-default.context-token.js';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
-import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/index.ts
new file mode 100644
index 0000000000..dc313f8b0b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/default/index.ts
@@ -0,0 +1,2 @@
+export { UMB_COLLECTION_CONTEXT } from './collection-default.context-token.js';
+export { UmbDefaultCollectionContext } from './collection-default.context.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts
index 93c0d2aceb..a6f8a60401 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts
@@ -7,6 +7,7 @@ export * from './collection.element.js';
export * from './components/index.js';
export * from './default/collection-default.context.js';
+export * from './default/collection-default.context-token.js';
export * from './collection-filter-model.interface.js';
export { UMB_COLLECTION_ALIAS_CONDITION } from './collection-alias.manifest.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/repository/collection-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/repository/collection-data-source.interface.ts
index 315782b7ab..3735c17e9d 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/repository/collection-data-source.interface.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/repository/collection-data-source.interface.ts
@@ -1,6 +1,10 @@
import type { UmbDataSourceResponse } from '../../repository/index.js';
import type { UmbPagedModel } from '../../repository/types.js';
+import type { UmbCollectionFilterModel } from '../collection-filter-model.interface.js';
-export interface UmbCollectionDataSource {
+export interface UmbCollectionDataSource<
+ CollectionItemType extends { entityType: string; unique: string } = any,
+ FilterType extends UmbCollectionFilterModel = UmbCollectionFilterModel,
+> {
getCollection(filter: FilterType): Promise>>;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/repository/collection-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/repository/collection-repository.interface.ts
index 49a73f110c..68396a73b8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/repository/collection-repository.interface.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/repository/collection-repository.interface.ts
@@ -1,5 +1,10 @@
+import type { UmbCollectionFilterModel } from '../collection-filter-model.interface.js';
+import type { UmbPagedModel, UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
-export interface UmbCollectionRepository extends UmbApi {
- requestCollection(filter?: any): Promise;
+export interface UmbCollectionRepository<
+ CollectionItemType extends { entityType: string; unique: string } = any,
+ FilterType extends UmbCollectionFilterModel = UmbCollectionFilterModel,
+> extends UmbApi {
+ requestCollection(filter?: FilterType): Promise>>;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts
index f35fb988fb..ecd2225151 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts
@@ -18,7 +18,6 @@ export interface UmbCollectionConfiguration {
orderBy?: string;
orderDirection?: string;
pageSize?: number;
- useInfiniteEditor?: boolean;
userDefinedProperties?: Array;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/header-app/header-app-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/header-app/header-app-button.element.ts
index c3b915fcc2..1ec7d1de7c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/header-app/header-app-button.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/header-app/header-app-button.element.ts
@@ -1,11 +1,12 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
-import { css, html, LitElement, customElement, ifDefined } from '@umbraco-cms/backoffice/external/lit';
+import { css, html, customElement, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type {
ManifestHeaderAppButtonKind,
UmbBackofficeManifestKind,
} from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
const manifest: UmbBackofficeManifestKind = {
type: 'kind',
@@ -21,7 +22,7 @@ const manifest: UmbBackofficeManifestKind = {
umbExtensionsRegistry.register(manifest);
@customElement('umb-header-app-button')
-export class UmbHeaderAppButtonElement extends LitElement {
+export class UmbHeaderAppButtonElement extends UmbLitElement {
public manifest?: ManifestHeaderAppButtonKind;
render() {
@@ -41,7 +42,11 @@ export class UmbHeaderAppButtonElement extends LitElement {
css`
uui-button {
font-size: 18px;
- --uui-button-background-color: transparent;
+ --uui-button-background-color: var(--umb-header-app-button-background-color, transparent);
+ --uui-button-background-color-hover: var(
+ --umb-header-app-button-background-color-hover,
+ var(--uui-color-emphasis)
+ );
}
`,
];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts
index 086630bcd5..96110d38c2 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts
@@ -25,7 +25,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
this._src = value.src;
}
get value(): MediaValueType {
- return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.unique };
+ return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.temporaryUnique };
}
/**
@@ -67,7 +67,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
async #onUpload(e: UUIFileDropzoneEvent) {
//Property Editor for Upload field will always only have one file.
const item: UmbTemporaryFileModel = {
- unique: UmbId.new(),
+ temporaryUnique: UmbId.new(),
file: e.detail.files[0],
};
const upload = this.#manager.uploadOne(item);
@@ -80,7 +80,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
const uploaded = await upload;
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
- this.temporaryFile = { unique: item.unique, file: item.file };
+ this.temporaryFile = { temporaryUnique: item.temporaryUnique, file: item.file };
this.dispatchEvent(new UmbChangeEvent());
}
}
@@ -172,6 +172,9 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
static styles = [
css`
+ :host {
+ position: relative;
+ }
uui-icon {
vertical-align: sub;
margin-right: var(--uui-size-space-4);
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/components/property-type-based-property/property-type-based-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/components/property-type-based-property/property-type-based-property.element.ts
index 0f1ea60615..18c34c7129 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/components/property-type-based-property/property-type-based-property.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/components/property-type-based-property/property-type-based-property.element.ts
@@ -1,5 +1,6 @@
import type { UmbPropertyEditorConfig } from '../../../property-editor/index.js';
import type { UmbPropertyTypeModel } from '../../types.js';
+import { UmbContentPropertyContext } from '@umbraco-cms/backoffice/content';
import type { UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type';
import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@@ -34,6 +35,8 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
private _dataTypeDetailRepository = new UmbDataTypeDetailRepository(this);
private _dataTypeObserver?: UmbObserverController;
+ #contentPropertyContext = new UmbContentPropertyContext(this);
+
private async _observeDataType(dataTypeUnique?: string) {
this._dataTypeObserver?.destroy();
if (dataTypeUnique) {
@@ -42,6 +45,9 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
this._dataTypeObserver = this.observe(
await this._dataTypeDetailRepository.byUnique(dataTypeUnique),
(dataType) => {
+ const contextValue = dataType ? { unique: dataType.unique } : undefined;
+ this.#contentPropertyContext.setDataType(contextValue);
+
this._dataTypeData = dataType?.values;
this._propertyEditorUiAlias = dataType?.editorUiAlias || undefined;
// If there is no UI, we will look up the Property editor model to find the default UI alias:
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/content-property.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/content-property.context-token.ts
new file mode 100644
index 0000000000..8e9bf8626a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/content-property.context-token.ts
@@ -0,0 +1,4 @@
+import type { UmbContentPropertyContext } from './content-property.context.js';
+import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
+
+export const UMB_CONTENT_PROPERTY_CONTEXT = new UmbContextToken('UmbContentPropertyContext');
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/content-property.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/content-property.context.ts
new file mode 100644
index 0000000000..1b7343c660
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/content-property.context.ts
@@ -0,0 +1,18 @@
+import type { UmbPropertyTypeModel } from '../content-type/types.js';
+import { UMB_CONTENT_PROPERTY_CONTEXT } from './content-property.context-token.js';
+import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
+import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
+import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
+
+export class UmbContentPropertyContext extends UmbContextBase {
+ #dataType = new UmbObjectState(undefined);
+ dataType = this.#dataType.asObservable();
+
+ constructor(host: UmbControllerHost) {
+ super(host, UMB_CONTENT_PROPERTY_CONTEXT);
+ }
+
+ setDataType(dataType: UmbPropertyTypeModel['dataType'] | undefined) {
+ this.#dataType.setValue(dataType);
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/index.ts
index 60f06132a7..3a311a911c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/content/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/index.ts
@@ -1 +1,4 @@
export * from './workspace/index.js';
+
+export { UmbContentPropertyContext } from './content-property.context.js';
+export { UMB_CONTENT_PROPERTY_CONTEXT } from './content-property.context-token.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/sort-children-of.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/sort-children-of.action.ts
index 06e4da6146..c2f34778ba 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/sort-children-of.action.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/sort-children-of.action.ts
@@ -1,9 +1,9 @@
import { UmbEntityActionBase } from '../../entity-action-base.js';
+import { UmbRequestReloadChildrenOfEntityEvent } from '../../request-reload-children-of-entity.event.js';
import { UMB_SORT_CHILDREN_OF_MODAL } from './modal/index.js';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { MetaEntityActionSortChildrenOfKind } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
-import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
export class UmbSortChildrenOfEntityAction extends UmbEntityActionBase {
async execute() {
@@ -22,7 +22,7 @@ export class UmbSortChildrenOfEntityAction extends UmbEntityActionBase;
#inputTimer?: NodeJS.Timeout;
#inputTimerAmount = 500;
@@ -29,15 +30,15 @@ export class UmbExtensionCollectionElement extends UmbCollectionDefaultElement {
}
#onChange(event: UUISelectEvent) {
- const extensionType = event.target.value;
+ const extensionType = event.target.value as string;
this.#collectionContext?.setFilter({ type: extensionType });
}
#onSearch(event: InputEvent) {
const target = event.target as HTMLInputElement;
- const query = target.value || '';
+ const filter = target.value || '';
clearTimeout(this.#inputTimer);
- this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ query }), this.#inputTimerAmount);
+ this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ filter }), this.#inputTimerAmount);
}
protected renderToolbar() {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/repository/extension-collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/repository/extension-collection.repository.ts
index 20c8a40094..c867c84bd2 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/repository/extension-collection.repository.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/repository/extension-collection.repository.ts
@@ -1,39 +1,45 @@
import { umbExtensionsRegistry } from '../../registry.js';
-import type { ManifestTypes } from '../../models/index.js';
+import type { UmbExtensionCollectionFilterModel, UmbExtensionDetailModel } from '../types.js';
+import { UMB_EXTENSION_ENTITY_TYPE } from '../../entity.js';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbCollectionRepository } from '@umbraco-cms/backoffice/collection';
-export interface UmbExtensionCollectionFilter {
- query?: string;
- skip: number;
- take: number;
- type?: ManifestTypes['type'];
-}
-
-export class UmbExtensionCollectionRepository extends UmbRepositoryBase implements UmbCollectionRepository {
+export class UmbExtensionCollectionRepository
+ extends UmbRepositoryBase
+ implements UmbCollectionRepository
+{
constructor(host: UmbControllerHost) {
super(host);
}
- async requestCollection(filter: UmbExtensionCollectionFilter) {
- let extensions = umbExtensionsRegistry.getAllExtensions();
+ async requestCollection(query: UmbExtensionCollectionFilterModel) {
+ let extensions: Array = umbExtensionsRegistry.getAllExtensions().map((manifest) => {
+ return {
+ ...manifest,
+ unique: manifest.alias,
+ entityType: UMB_EXTENSION_ENTITY_TYPE,
+ };
+ });
- if (filter.query) {
- const query = filter.query.toLowerCase();
+ const skip = query.skip || 0;
+ const take = query.take || 100;
+
+ if (query.filter) {
+ const text = query.filter.toLowerCase();
extensions = extensions.filter(
- (x) => x.name.toLowerCase().includes(query) || x.alias.toLowerCase().includes(query),
+ (x) => x.name.toLowerCase().includes(text) || x.alias.toLowerCase().includes(text),
);
}
- if (filter.type) {
- extensions = extensions.filter((x) => x.type === filter.type);
+ if (query.type) {
+ extensions = extensions.filter((x) => x.type === query.type);
}
extensions.sort((a, b) => a.type.localeCompare(b.type) || a.alias.localeCompare(b.alias));
const total = extensions.length;
- const items = extensions.slice(filter.skip, filter.skip + filter.take);
+ const items = extensions.slice(skip, skip + take);
const data = { items, total };
return { data };
}
@@ -41,4 +47,4 @@ export class UmbExtensionCollectionRepository extends UmbRepositoryBase implemen
destroy(): void {}
}
-export default UmbExtensionCollectionRepository;
+export { UmbExtensionCollectionRepository as api };
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/types.ts
new file mode 100644
index 0000000000..6090c089e0
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/types.ts
@@ -0,0 +1,12 @@
+import type { UmbExtensionEntityType } from '../entity.js';
+import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection';
+import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
+
+export interface UmbExtensionCollectionFilterModel extends UmbCollectionFilterModel {
+ type?: string;
+}
+
+export interface UmbExtensionDetailModel extends ManifestBase {
+ unique: string;
+ entityType: UmbExtensionEntityType;
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/extension-table-action-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/extension-table-action-column-layout.element.ts
deleted file mode 100644
index a647801b7e..0000000000
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/extension-table-action-column-layout.element.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { umbExtensionsRegistry } from '../../index.js';
-import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
-import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
-import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
-import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
-import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
-
-@customElement('umb-extension-table-action-column-layout')
-export class UmbExtensionTableActionColumnLayoutElement extends UmbLitElement {
- @property({ attribute: false })
- value!: ManifestBase;
-
- #collectionContext?: UmbDefaultCollectionContext;
-
- constructor() {
- super();
-
- this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => {
- this.#collectionContext = instance;
- });
- }
-
- async #removeExtension() {
- await umbConfirmModal(this, {
- headline: 'Unload extension',
- confirmLabel: 'Unload',
- content: html`Are you sure you want to unload the extension ${this.value.alias}?
`,
- color: 'danger',
- });
-
- umbExtensionsRegistry.unregister(this.value.alias);
-
- this.#collectionContext?.requestCollection();
- }
-
- render() {
- return html`
-
-
-
- `;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'umb-extension-table-action-column-layout': UmbExtensionTableActionColumnLayoutElement;
- }
-}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/table/extension-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/table/extension-table-collection-view.element.ts
index 302fb89c2c..63875a9d04 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/table/extension-table-collection-view.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/table/extension-table-collection-view.element.ts
@@ -1,12 +1,12 @@
+import type { UmbExtensionCollectionFilterModel, UmbExtensionDetailModel } from '../../types.js';
import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import type { UmbTableColumn, UmbTableConfig, UmbTableItem } from '@umbraco-cms/backoffice/components';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
-import '../extension-table-action-column-layout.element.js';
+import './extension-table-entity-actions-column-layout.element.js';
@customElement('umb-extension-table-collection-view')
export class UmbExtensionTableCollectionViewElement extends UmbLitElement {
@@ -36,14 +36,14 @@ export class UmbExtensionTableCollectionViewElement extends UmbLitElement {
{
name: '',
alias: 'extensionAction',
- elementName: 'umb-extension-table-action-column-layout',
+ elementName: 'umb-extension-table-entity-actions-column-layout',
},
];
@state()
private _tableItems: Array = [];
- #collectionContext?: UmbDefaultCollectionContext;
+ #collectionContext?: UmbDefaultCollectionContext;
constructor() {
super();
@@ -59,10 +59,10 @@ export class UmbExtensionTableCollectionViewElement extends UmbLitElement {
this.observe(this.#collectionContext.items, (items) => this.#createTableItems(items), 'umbCollectionItemsObserver');
}
- #createTableItems(extensions: Array) {
+ #createTableItems(extensions: Array) {
this._tableItems = extensions.map((extension) => {
return {
- id: extension.alias,
+ id: extension.unique,
data: [
{
columnAlias: 'extensionType',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/table/extension-table-entity-actions-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/table/extension-table-entity-actions-column-layout.element.ts
new file mode 100644
index 0000000000..bec7683247
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/table/extension-table-entity-actions-column-layout.element.ts
@@ -0,0 +1,35 @@
+import type { UmbExtensionDetailModel } from '../../types.js';
+import { html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+
+const elementName = 'umb-extension-table-entity-actions-column-layout';
+@customElement(elementName)
+export class UmbExtensionTableEntityActionsColumnLayoutElement extends UmbLitElement {
+ @property({ attribute: false })
+ value!: UmbExtensionDetailModel;
+
+ @state()
+ _isOpen = false;
+
+ #onActionExecuted() {
+ this._isOpen = false;
+ }
+
+ render() {
+ return html`
+
+
+
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [elementName]: UmbExtensionTableEntityActionsColumnLayoutElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/manifests.ts
new file mode 100644
index 0000000000..99d492d965
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/manifests.ts
@@ -0,0 +1,3 @@
+import { manifests as unregisterManifests } from './unregister/manifests.js';
+
+export const manifests = [...unregisterManifests];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/unregister/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/unregister/index.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/unregister/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/unregister/manifests.ts
new file mode 100644
index 0000000000..2493554788
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/unregister/manifests.ts
@@ -0,0 +1,16 @@
+import { UMB_EXTENSION_ENTITY_TYPE } from '../../entity.js';
+
+export const manifests = [
+ {
+ type: 'entityAction',
+ kind: 'default',
+ alias: 'Umb.EntityAction.Extension.Unregister',
+ name: 'Unregister Extension Entity Action',
+ api: () => import('./unregister-extension.action.js'),
+ forEntityTypes: [UMB_EXTENSION_ENTITY_TYPE],
+ meta: {
+ label: 'Unregister',
+ icon: 'icon-trash',
+ },
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/unregister/unregister-extension.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/unregister/unregister-extension.action.ts
new file mode 100644
index 0000000000..59b2dd8ccb
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity-actions/unregister/unregister-extension.action.ts
@@ -0,0 +1,33 @@
+import { umbExtensionsRegistry } from '../../registry.js';
+import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
+import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
+import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
+import { html } from '@umbraco-cms/backoffice/external/lit';
+
+export class UmbUnregisterExtensionEntityAction extends UmbEntityActionBase {
+ async execute() {
+ if (!this.args.unique) throw new Error('Cannot delete an item without a unique identifier.');
+
+ const extension = umbExtensionsRegistry.getByAlias(this.args.unique);
+ if (!extension) throw new Error('Extension not found');
+
+ await umbConfirmModal(this, {
+ headline: 'Unregister extension',
+ confirmLabel: 'Unregister',
+ content: html`Are you sure you want to unregister the extension ${extension.alias}?
`,
+ color: 'danger',
+ });
+
+ umbExtensionsRegistry.unregister(extension.alias);
+
+ const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
+ const event = new UmbRequestReloadStructureForEntityEvent({
+ unique: this.args.unique,
+ entityType: this.args.entityType,
+ });
+
+ actionEventContext.dispatchEvent(event);
+ }
+}
+
+export { UmbUnregisterExtensionEntityAction as api };
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity.ts
new file mode 100644
index 0000000000..5d12118ed8
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/entity.ts
@@ -0,0 +1,3 @@
+export const UMB_EXTENSION_ENTITY_TYPE = 'extension';
+
+export type UmbExtensionEntityType = typeof UMB_EXTENSION_ENTITY_TYPE;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/manifests.ts
index ab3b172689..dd1a2d6e84 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/manifests.ts
@@ -2,6 +2,7 @@ import { manifests as conditionManifests } from './conditions/manifests.js';
import { manifests as menuItemManifests } from './menu-item/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as collectionManifests } from './collection/manifests.js';
+import { manifests as entityActionManifests } from './entity-actions/manifests.js';
import type { ManifestTypes } from './models/index.js';
export const manifests: Array = [
@@ -9,4 +10,5 @@ export const manifests: Array = [
...menuItemManifests,
...workspaceManifests,
...collectionManifests,
+ ...entityActionManifests,
];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts
index f7b427da73..c81ff73f7f 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts
@@ -33,6 +33,7 @@ import type { ManifestMenu } from './menu.model.js';
import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.model.js';
import type { ManifestModal } from './modal.model.js';
import type { ManifestPackageView } from './package-view.model.js';
+import type { ManifestPreviewAppProvider } from './preview-app.model.js';
import type { ManifestPropertyAction, ManifestPropertyActionDefaultKind } from './property-action.model.js';
import type { ManifestPropertyEditorUi, ManifestPropertyEditorSchema } from './property-editor.model.js';
import type { ManifestRepository } from './repository.model.js';
@@ -97,6 +98,7 @@ export type * from './mfa-login-provider.model.js';
export type * from './modal.model.js';
export type * from './monaco-markdown-editor-action.model.js';
export type * from './package-view.model.js';
+export type * from './preview-app.model.js';
export type * from './property-action.model.js';
export type * from './property-editor.model.js';
export type * from './repository.model.js';
@@ -183,6 +185,7 @@ export type ManifestTypes =
| ManifestModal
| ManifestMonacoMarkdownEditorAction
| ManifestPackageView
+ | ManifestPreviewAppProvider
| ManifestPropertyActions
| ManifestPropertyEditorSchema
| ManifestPropertyEditorUi
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/preview-app.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/preview-app.model.ts
new file mode 100644
index 0000000000..d2c0fcc44c
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/preview-app.model.ts
@@ -0,0 +1,8 @@
+import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
+
+/**
+ * Preview apps are displayed in the menu of the preview window.
+ */
+export interface ManifestPreviewAppProvider extends ManifestElement {
+ type: 'previewApp';
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json
index b573765d43..1306241f52 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icon-dictionary.json
@@ -2404,6 +2404,10 @@
"name": "icon-window-popin",
"file": "square-arrow-down-left.svg"
},
+ {
+ "name": "icon-window-popout",
+ "file": "square-arrow-up-right.svg"
+ },
{
"name": "icon-window-sizes",
"file": "scaling.svg"
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-window-popout.js b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-window-popout.js
new file mode 100644
index 0000000000..16eeec336b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-window-popout.js
@@ -0,0 +1,16 @@
+export default `
+
+`;
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icons.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icons.ts
index 6a79e57053..ef496478b3 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icons.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icons.ts
@@ -2055,6 +2055,10 @@ name: "icon-window-popin",
path: "./icons/icon-window-popin.js",
},{
+name: "icon-window-popout",
+
+path: "./icons/icon-window-popout.js",
+},{
name: "icon-window-sizes",
path: "./icons/icon-window-sizes.js",
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-layout/property-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-layout/property-layout.element.ts
index cdc2c5fc07..9ef199cef8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-layout/property-layout.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-layout/property-layout.element.ts
@@ -104,7 +104,7 @@ export class UmbPropertyLayoutElement extends LitElement {
height: min-content;
}
/*@container (width > 600px) {*/
- #headerColumn {
+ :host(:not([orientation='vertical'])) #headerColumn {
position: sticky;
top: calc(var(--uui-size-space-2) * -1);
}
@@ -128,7 +128,7 @@ export class UmbPropertyLayoutElement extends LitElement {
margin-top: var(--uui-size-space-3);
}
/*@container (width > 600px) {*/
- #editorColumn {
+ :host(:not([orientation='vertical'])) #editorColumn {
margin-top: 0;
}
/*}*/
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.element.ts
index 920b36a841..73ee766494 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.element.ts
@@ -235,7 +235,10 @@ export class UmbPropertyElement extends UmbLitElement {
this.#valueObserver = this.observe(
this.#propertyContext.value,
(value) => {
+ // Set the value on the element:
this._element!.value = value;
+ // Set the value on the context as well, to ensure that any default values are stored right away:
+ this.#propertyContext.setValue(value);
if (this.#validationMessageBinder) {
this.#validationMessageBinder.value = value;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/empty-recycle-bin/empty-recycle-bin.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/empty-recycle-bin/empty-recycle-bin.action.ts
index acc0562252..bfe41c760a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/empty-recycle-bin/empty-recycle-bin.action.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/empty-recycle-bin/empty-recycle-bin.action.ts
@@ -6,7 +6,7 @@ import {
type MetaEntityActionEmptyRecycleBinKind,
} from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
-import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree';
+import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
/**
* Entity action for emptying the recycle bin.
@@ -34,7 +34,7 @@ export class UmbEmptyRecycleBinEntityAction extends UmbEntityActionBase extends UmbControllerBase {
#temporaryFileRepository;
- #queue = new UmbArrayState([], (item) => item.unique);
+ #queue = new UmbArrayState([], (item) => item.temporaryUnique);
public readonly queue = this.#queue.asObservable();
constructor(host: UmbControllerHost) {
@@ -66,18 +65,18 @@ export class UmbTemporaryFileManager<
if (!queue.length) return filesCompleted;
for (const item of queue) {
- if (!item.unique) throw new Error(`Unique is missing for item ${item}`);
+ if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`);
- const { error } = await this.#temporaryFileRepository.upload(item.unique, item.file);
+ const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, item.file);
//await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown
let status: TemporaryFileStatus;
if (error) {
status = TemporaryFileStatus.ERROR;
- this.#queue.updateOne(item.unique, { ...item, status });
+ this.#queue.updateOne(item.temporaryUnique, { ...item, status });
} else {
status = TemporaryFileStatus.SUCCESS;
- this.#queue.updateOne(item.unique, { ...item, status });
+ this.#queue.updateOne(item.temporaryUnique, { ...item, status });
}
filesCompleted.push({ ...item, status });
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts
index 18f76771fd..a909305c8d 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts
@@ -23,25 +23,30 @@ export interface UmbTreeDataSourceConstructor {
+export interface UmbTreeDataSource<
+ TreeItemType extends UmbTreeItemModel,
+ TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
+ TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
+ TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
+> {
/**
* Gets the root items of the tree.
* @return {*} {Promise>>}
* @memberof UmbTreeDataSource
*/
- getRootItems(args: UmbTreeRootItemsRequestArgs): Promise>>;
+ getRootItems(args: TreeRootItemsRequestArgsType): Promise>>;
/**
* Gets the children of the given parent item.
* @return {*} {Promise>}
* @memberof UmbTreeDataSource
*/
- getChildrenOf(args: UmbTreeChildrenOfRequestArgs): Promise>>;
+ getChildrenOf(args: TreeChildrenOfRequestArgsType): Promise>>;
/**
* Gets the ancestors of the given item.
* @return {*} {Promise>}
* @memberof UmbTreeDataSource
*/
- getAncestorsOf(args: UmbTreeAncestorsOfRequestArgs): Promise>>;
+ getAncestorsOf(args: TreeAncestorsOfRequestArgsType): Promise>>;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts
index bbb20fe312..bbd9469bd9 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts
@@ -19,7 +19,7 @@ import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
* @abstract
* @class UmbTreeRepositoryBase
* @extends {UmbRepositoryBase}
- * @implements {UmbTreeRepository}
+ * @implements {UmbTreeRepository}
* @implements {UmbApi}
* @template TreeItemType
* @template TreeRootType
@@ -27,9 +27,20 @@ import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export abstract class UmbTreeRepositoryBase<
TreeItemType extends UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel,
+ TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
+ TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
+ TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
>
extends UmbRepositoryBase
- implements UmbTreeRepository, UmbApi
+ implements
+ UmbTreeRepository<
+ TreeItemType,
+ TreeRootType,
+ TreeRootItemsRequestArgsType,
+ TreeChildrenOfRequestArgsType,
+ TreeAncestorsOfRequestArgsType
+ >,
+ UmbApi
{
protected _init: Promise;
protected _treeStore?: UmbTreeStore;
@@ -67,7 +78,7 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
- async requestRootTreeItems(args: UmbTreeRootItemsRequestArgs) {
+ async requestTreeRootItems(args: TreeRootItemsRequestArgsType) {
await this._init;
const { data, error: _error } = await this._treeSource.getRootItems(args);
@@ -85,7 +96,7 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
- async requestTreeItemsOf(args: UmbTreeChildrenOfRequestArgs) {
+ async requestTreeItemsOf(args: TreeChildrenOfRequestArgsType) {
if (!args.parent) throw new Error('Parent is missing');
if (args.parent.unique === undefined) throw new Error('Parent unique is missing');
if (args.parent.entityType === null) throw new Error('Parent entity type is missing');
@@ -106,7 +117,7 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
- async requestTreeItemAncestors(args: UmbTreeAncestorsOfRequestArgs) {
+ async requestTreeItemAncestors(args: TreeAncestorsOfRequestArgsType) {
if (args.treeItem.unique === undefined) throw new Error('Descendant unique is missing');
await this._init;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts
index 17903eb7e1..186b593fe5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts
@@ -20,6 +20,9 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export interface UmbTreeRepository<
TreeItemType extends UmbTreeItemModel = UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel = UmbTreeRootModel,
+ TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
+ TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
+ TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
> extends UmbApi {
/**
* Requests the root of the tree.
@@ -35,7 +38,7 @@ export interface UmbTreeRepository<
* @param {UmbTreeRootItemsRequestArgs} args
* @memberof UmbTreeRepository
*/
- requestRootTreeItems: (args: UmbTreeRootItemsRequestArgs) => Promise<{
+ requestTreeRootItems: (args: TreeRootItemsRequestArgsType) => Promise<{
data?: UmbPagedModel;
error?: ProblemDetails;
asObservable?: () => Observable;
@@ -46,7 +49,7 @@ export interface UmbTreeRepository<
* @param {UmbTreeChildrenOfRequestArgs} args
* @memberof UmbTreeRepository
*/
- requestTreeItemsOf: (args: UmbTreeChildrenOfRequestArgs) => Promise<{
+ requestTreeItemsOf: (args: TreeChildrenOfRequestArgsType) => Promise<{
data?: UmbPagedModel;
error?: ProblemDetails;
asObservable?: () => Observable;
@@ -58,7 +61,7 @@ export interface UmbTreeRepository<
* @memberof UmbTreeRepository
*/
requestTreeItemAncestors: (
- args: UmbTreeAncestorsOfRequestArgs,
+ args: TreeAncestorsOfRequestArgsType,
) => Promise<{ data?: TreeItemType[]; error?: ProblemDetails; asObservable?: () => Observable }>;
/**
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts
index c95af216a8..ff238113a0 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts
@@ -12,10 +12,13 @@ import type { UmbPagedModel } from '@umbraco-cms/backoffice/repository';
export interface UmbTreeServerDataSourceBaseArgs<
ServerTreeItemType extends { hasChildren: boolean },
ClientTreeItemType extends UmbTreeItemModelBase,
+ TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
+ TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
+ TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
> {
- getRootItems: (args: UmbTreeRootItemsRequestArgs) => Promise>;
- getChildrenOf: (args: UmbTreeChildrenOfRequestArgs) => Promise>;
- getAncestorsOf: (args: UmbTreeAncestorsOfRequestArgs) => Promise>;
+ getRootItems: (args: TreeRootItemsRequestArgsType) => Promise>;
+ getChildrenOf: (args: TreeChildrenOfRequestArgsType) => Promise>;
+ getAncestorsOf: (args: TreeAncestorsOfRequestArgsType) => Promise>;
mapper: (item: ServerTreeItemType) => ClientTreeItemType;
}
@@ -28,7 +31,16 @@ export interface UmbTreeServerDataSourceBaseArgs<
export abstract class UmbTreeServerDataSourceBase<
ServerTreeItemType extends { hasChildren: boolean },
ClientTreeItemType extends UmbTreeItemModel,
-> implements UmbTreeDataSource
+ TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
+ TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
+ TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
+> implements
+ UmbTreeDataSource<
+ ClientTreeItemType,
+ TreeRootItemsRequestArgsType,
+ TreeChildrenOfRequestArgsType,
+ TreeAncestorsOfRequestArgsType
+ >
{
#host;
#getRootItems;
@@ -41,7 +53,16 @@ export abstract class UmbTreeServerDataSourceBase<
* @param {UmbControllerHost} host
* @memberof UmbTreeServerDataSourceBase
*/
- constructor(host: UmbControllerHost, args: UmbTreeServerDataSourceBaseArgs) {
+ constructor(
+ host: UmbControllerHost,
+ args: UmbTreeServerDataSourceBaseArgs<
+ ServerTreeItemType,
+ ClientTreeItemType,
+ TreeRootItemsRequestArgsType,
+ TreeChildrenOfRequestArgsType,
+ TreeAncestorsOfRequestArgsType
+ >,
+ ) {
this.#host = host;
this.#getRootItems = args.getRootItems;
this.#getChildrenOf = args.getChildrenOf;
@@ -55,7 +76,7 @@ export abstract class UmbTreeServerDataSourceBase<
* @return {*}
* @memberof UmbTreeServerDataSourceBase
*/
- async getRootItems(args: UmbTreeRootItemsRequestArgs) {
+ async getRootItems(args: TreeRootItemsRequestArgsType) {
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getRootItems(args));
if (data) {
@@ -72,7 +93,7 @@ export abstract class UmbTreeServerDataSourceBase<
* @return {*}
* @memberof UmbTreeServerDataSourceBase
*/
- async getChildrenOf(args: UmbTreeChildrenOfRequestArgs) {
+ async getChildrenOf(args: TreeChildrenOfRequestArgsType) {
if (args.parent.unique === undefined) throw new Error('Parent unique is missing');
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getChildrenOf(args));
@@ -91,7 +112,7 @@ export abstract class UmbTreeServerDataSourceBase<
* @return {*}
* @memberof UmbTreeServerDataSourceBase
*/
- async getAncestorsOf(args: UmbTreeAncestorsOfRequestArgs) {
+ async getAncestorsOf(args: TreeAncestorsOfRequestArgsType) {
if (!args.treeItem.entityType) throw new Error('Parent unique is missing');
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAncestorsOf(args));
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context-token.ts
new file mode 100644
index 0000000000..c5b9436c00
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context-token.ts
@@ -0,0 +1,7 @@
+import type { UmbTreeItemModel, UmbTreeRootModel } from '../types.js';
+import type { UmbDefaultTreeContext } from './default-tree.context.js';
+import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
+
+export const UMB_TREE_CONTEXT = new UmbContextToken>(
+ 'UmbTreeContext',
+);
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts
index d5311e95b5..5e5709f5fe 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts
@@ -2,6 +2,8 @@ import { UmbRequestReloadTreeItemChildrenEvent } from '../reload-tree-item-child
import type { UmbTreeItemModel, UmbTreeRootModel, UmbTreeStartNode } from '../types.js';
import type { UmbTreeRepository } from '../data/tree-repository.interface.js';
import type { UmbTreeContext } from '../tree-context.interface.js';
+import type { UmbTreeRootItemsRequestArgs } from '../data/types.js';
+import { UMB_TREE_CONTEXT } from './default-tree.context-token.js';
import { type UmbActionEventContext, UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import {
type ManifestRepository,
@@ -12,15 +14,24 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbPaginationManager, UmbSelectionManager, debounce } from '@umbraco-cms/backoffice/utils';
-import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
+import {
+ UmbRequestReloadChildrenOfEntityEvent,
+ type UmbEntityActionEvent,
+} from '@umbraco-cms/backoffice/entity-action';
import { UmbArrayState, UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
-import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
-export class UmbDefaultTreeContext
- extends UmbContextBase>
+export class UmbDefaultTreeContext<
+ TreeItemType extends UmbTreeItemModel,
+ TreeRootType extends UmbTreeRootModel,
+ RequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
+ >
+ extends UmbContextBase>
implements UmbTreeContext
{
+ #additionalRequestArgs = new UmbObjectState | object>({});
+ public readonly additionalRequestArgs = this.#additionalRequestArgs.asObservable();
+
#treeRoot = new UmbObjectState(undefined);
treeRoot = this.#treeRoot.asObservable();
@@ -57,7 +68,7 @@ export class UmbDefaultTreeContext this.#debouncedLoadTree(true);
#debouncedLoadTree(reload = false) {
- if (this.getStartFrom()) {
+ if (this.getStartNode()) {
this.#loadRootItems(reload);
return;
}
@@ -166,10 +177,12 @@ export class UmbDefaultTreeContext) {
+ this.#additionalRequestArgs.setValue({ ...this.#additionalRequestArgs.getValue(), ...args });
+ this.#resetTree();
+ this.loadTree();
+ }
+
+ public getAdditionalRequestArgs() {
+ return this.#additionalRequestArgs.getValue();
+ }
+
/**
* Gets the startNode config
* @return {UmbTreeStartNode}
* @memberof UmbDefaultTreeContext
*/
- getStartFrom() {
+ getStartNode() {
return this.#startNode.getValue();
}
@@ -245,14 +272,26 @@ export class UmbDefaultTreeContext {
this.#actionEventContext = instance;
+
this.#actionEventContext.removeEventListener(
UmbRequestReloadTreeItemChildrenEvent.TYPE,
this.#onReloadRequest as EventListener,
);
+
+ this.#actionEventContext.removeEventListener(
+ UmbRequestReloadChildrenOfEntityEvent.TYPE,
+ this.#onReloadRequest as EventListener,
+ );
+
this.#actionEventContext.addEventListener(
UmbRequestReloadTreeItemChildrenEvent.TYPE,
this.#onReloadRequest as EventListener,
);
+
+ this.#actionEventContext.addEventListener(
+ UmbRequestReloadChildrenOfEntityEvent.TYPE,
+ this.#onReloadRequest as EventListener,
+ );
});
}
@@ -291,12 +330,14 @@ export class UmbDefaultTreeContext>(
- 'UmbTreeContext',
-);
+export { UmbDefaultTreeContext as api };
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts
index 9123a01161..bbef0c7435 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts
@@ -6,7 +6,7 @@ import type {
UmbTreeStartNode,
} from '../types.js';
import type { UmbDefaultTreeContext } from './default-tree.context.js';
-import { UMB_DEFAULT_TREE_CONTEXT } from './default-tree.context.js';
+import { UMB_TREE_CONTEXT } from './default-tree.context-token.js';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { html, nothing, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -57,7 +57,7 @@ export class UmbDefaultTreeElement extends UmbLitElement {
this.#init = Promise.all([
// TODO: Notice this can be retrieve via a api property. [NL]
- this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (instance) => {
+ this.consumeContext(UMB_TREE_CONTEXT, (instance) => {
this.#treeContext = instance;
this.observe(this.#treeContext.treeRoot, (treeRoot) => (this._treeRoot = treeRoot));
this.observe(this.#treeContext.rootItems, (rootItems) => (this._rootItems = rootItems));
@@ -80,7 +80,7 @@ export class UmbDefaultTreeElement extends UmbLitElement {
}
if (_changedProperties.has('startNode')) {
- this.#treeContext!.setStartFrom(this.startNode);
+ this.#treeContext!.setStartNode(this.startNode);
}
if (_changedProperties.has('hideTreeRoot')) {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/index.ts
index b81a955993..f453c85ea8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/index.ts
@@ -1,2 +1,3 @@
-export { UmbDefaultTreeElement as UmbTreeDefaultElement } from './default-tree.element.js';
-export { UmbDefaultTreeContext as UmbTreeDefaultContext } from './default-tree.context.js';
+export { UmbDefaultTreeElement } from './default-tree.element.js';
+export { UmbDefaultTreeContext } from './default-tree.context.js';
+export { UMB_TREE_CONTEXT } from './default-tree.context-token.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.ts
index aa785f0841..20b8a8619a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.ts
@@ -1,8 +1,8 @@
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
-import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
+import { UmbEntityActionBase, UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import type { MetaEntityActionFolderKind } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
-import { UMB_FOLDER_CREATE_MODAL, UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
+import { UMB_FOLDER_CREATE_MODAL } from '@umbraco-cms/backoffice/tree';
export class UmbCreateFolderEntityAction extends UmbEntityActionBase {
async execute() {
@@ -20,7 +20,7 @@ export class UmbCreateFolderEntityAction extends UmbEntityActionBase {
+ this.consumeContext(UMB_TREE_CONTEXT, (treeContext) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.treeContext = treeContext;
@@ -243,6 +248,11 @@ export abstract class UmbTreeItemContextBase<
this.#onReloadRequest as EventListener,
);
+ this.#actionEventContext?.removeEventListener(
+ UmbRequestReloadChildrenOfEntityEvent.TYPE,
+ this.#onReloadRequest as EventListener,
+ );
+
this.#actionEventContext?.removeEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
@@ -255,6 +265,11 @@ export abstract class UmbTreeItemContextBase<
this.#onReloadRequest as EventListener,
);
+ this.#actionEventContext.addEventListener(
+ UmbRequestReloadChildrenOfEntityEvent.TYPE,
+ this.#onReloadRequest as EventListener,
+ );
+
this.#actionEventContext.addEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
@@ -382,6 +397,12 @@ export abstract class UmbTreeItemContextBase<
UmbRequestReloadTreeItemChildrenEvent.TYPE,
this.#onReloadRequest as EventListener,
);
+
+ this.#actionEventContext?.removeEventListener(
+ UmbRequestReloadChildrenOfEntityEvent.TYPE,
+ this.#onReloadRequest as EventListener,
+ );
+
this.#actionEventContext?.removeEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-collection/workspace-view-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-collection/workspace-view-collection.element.ts
index 8a80c96069..d7c4ba65d2 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-collection/workspace-view-collection.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-collection/workspace-view-collection.element.ts
@@ -67,7 +67,6 @@ export class UmbWorkspaceViewCollectionElement extends UmbLitElement implements
orderBy: config?.getValueByAlias('orderBy') ?? 'updateDate',
orderDirection: config?.getValueByAlias('orderDirection') ?? 'asc',
pageSize: Number(config?.getValueByAlias('pageSize')) ?? 50,
- useInfiniteEditor: config?.getValueByAlias('useInfiniteEditor') ?? false,
userDefinedProperties: config?.getValueByAlias('includeProperties'),
};
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts
index d95b3b83cf..9164fb56d0 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts
@@ -27,8 +27,10 @@ import type {
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_PROPERTY_EDITOR_SCHEMA_ALIAS_DEFAULT } from '@umbraco-cms/backoffice/property-editor';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
-import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
-import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
+import {
+ UmbRequestReloadChildrenOfEntityEvent,
+ UmbRequestReloadStructureForEntityEvent,
+} from '@umbraco-cms/backoffice/entity-action';
type EntityType = UmbDataTypeDetailModel;
export class UmbDataTypeWorkspaceContext
@@ -347,7 +349,7 @@ export class UmbDataTypeWorkspaceContext
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
- const event = new UmbRequestReloadTreeItemChildrenEvent({
+ const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: parent.entityType,
unique: parent.unique,
});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts
index 7ca3b32fc8..fc68e45a2d 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts
@@ -10,9 +10,11 @@ import {
} from '@umbraco-cms/backoffice/workspace';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
-import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
-import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
+import {
+ UmbRequestReloadChildrenOfEntityEvent,
+ UmbRequestReloadStructureForEntityEvent,
+} from '@umbraco-cms/backoffice/entity-action';
export class UmbDictionaryWorkspaceContext
extends UmbSubmittableWorkspaceContextBase
@@ -146,7 +148,7 @@ export class UmbDictionaryWorkspaceContext
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
- const event = new UmbRequestReloadTreeItemChildrenEvent({
+ const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: parent.entityType,
unique: parent.unique,
});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts
index 17a0983d75..c71640cb62 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts
@@ -25,8 +25,10 @@ import {
UmbDocumentTypeDetailRepository,
} from '@umbraco-cms/backoffice/document-type';
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
-import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
-import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
+import {
+ UmbRequestReloadChildrenOfEntityEvent,
+ UmbRequestReloadStructureForEntityEvent,
+} from '@umbraco-cms/backoffice/entity-action';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -407,7 +409,7 @@ export class UmbDocumentBlueprintWorkspaceContext
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
- const event = new UmbRequestReloadTreeItemChildrenEvent({
+ const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: parent.entityType,
unique: parent.unique,
});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts
index 5b8d881c09..d4aa7b5813 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts
@@ -11,8 +11,10 @@ import {
import { UmbDocumentTypeWorkspaceEditorElement } from './document-type-workspace-editor.element.js';
import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
-import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
-import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
+import {
+ UmbRequestReloadChildrenOfEntityEvent,
+ UmbRequestReloadStructureForEntityEvent,
+} from '@umbraco-cms/backoffice/entity-action';
import {
UmbSubmittableWorkspaceContextBase,
UmbWorkspaceIsNewRedirectController,
@@ -295,7 +297,7 @@ export class UmbDocumentTypeWorkspaceContext
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
- const event = new UmbRequestReloadTreeItemChildrenEvent({
+ const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: parent.entityType,
unique: parent.unique,
});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts
index 7948afae45..14d1f17750 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts
@@ -1,7 +1,7 @@
+import { UMB_DOCUMENT_COLLECTION_CONTEXT } from '../document-collection.context-token.js';
import { css, customElement, html, map, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbDocumentTypeStructureRepository } from '@umbraco-cms/backoffice/document-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import {
UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN,
UMB_DOCUMENT_ENTITY_TYPE,
@@ -35,9 +35,6 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
@state()
private _rootPathName?: string;
- @state()
- private _useInfiniteEditor = false;
-
@property({ attribute: false })
manifest?: ManifestCollectionAction;
@@ -64,16 +61,13 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
});
});
- this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
+ this.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {
this.observe(collectionContext.view.currentView, (currentView) => {
this._currentView = currentView?.meta.pathName;
});
this.observe(collectionContext.view.rootPathName, (rootPathName) => {
this._rootPathName = rootPathName;
});
- this.observe(collectionContext.filter, (filter) => {
- this._useInfiniteEditor = filter.useInfiniteEditor == true;
- });
});
}
@@ -99,22 +93,14 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
}
#getCreateUrl(item: UmbAllowedDocumentTypeModel) {
- if (this._useInfiniteEditor) {
- return (
- this._createDocumentPath.replace(`${this._rootPathName}`, `${this._rootPathName}/${this._currentView}`) +
- UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({
- parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
- parentUnique: this._documentUnique ?? 'null',
- documentTypeUnique: item.unique,
- })
- );
- }
-
- return UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateAbsolute({
- parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
- parentUnique: this._documentUnique ?? 'null',
- documentTypeUnique: item.unique,
- });
+ return (
+ this._createDocumentPath.replace(`${this._rootPathName}`, `${this._rootPathName}/${this._currentView}`) +
+ UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({
+ parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
+ parentUnique: this._documentUnique ?? 'null',
+ documentTypeUnique: item.unique,
+ })
+ );
}
render() {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/document-collection.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/document-collection.context-token.ts
new file mode 100644
index 0000000000..570a5e35b0
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/document-collection.context-token.ts
@@ -0,0 +1,6 @@
+import type { UmbDocumentCollectionContext } from './document-collection.context.js';
+import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
+
+export const UMB_DOCUMENT_COLLECTION_CONTEXT = new UmbContextToken(
+ 'UmbCollectionContext',
+);
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts
index 0cc2874dea..cbc5640097 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/grid/document-grid-collection-view.element.ts
@@ -2,11 +2,11 @@ import { getPropertyValueByAlias } from '../index.js';
import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../paths.js';
import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js';
import type { UmbDocumentCollectionFilterModel, UmbDocumentCollectionItemModel } from '../../types.js';
+import { UMB_DOCUMENT_COLLECTION_CONTEXT } from '../../document-collection.context-token.js';
import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
import { fromCamelCase } from '@umbraco-cms/backoffice/utils';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
-import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui';
@@ -33,7 +33,7 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
constructor() {
super();
- this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
+ this.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {
this.#collectionContext = collectionContext;
this.#observeCollectionContext();
});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts
index 73b81cbc49..ab12a1b60c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/document-table-collection-view.element.ts
@@ -3,10 +3,10 @@ import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../paths.js';
import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js';
import type { UmbDocumentCollectionItemModel } from '../../types.js';
import type { UmbDocumentCollectionContext } from '../../document-collection.context.js';
+import { UMB_DOCUMENT_COLLECTION_CONTEXT } from '../../document-collection.context-token.js';
import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
-import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/modal';
import type {
@@ -68,7 +68,7 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
constructor() {
super();
- this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
+ this.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {
this.#collectionContext = collectionContext;
});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts
index 059d7cfbdc..242cf881eb 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts
@@ -90,9 +90,6 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement,
@property({ type: Boolean })
showOpenButton?: boolean;
- @property({ type: Boolean })
- ignoreUserStartNodes?: boolean;
-
@property()
public set value(idsString: string) {
this.selection = splitStringToArray(idsString);
@@ -153,7 +150,6 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement,
};
#openPicker() {
- // TODO: Configure the content picker, with `startNodeId` and `ignoreUserStartNodes` [LK]
this.#pickerContext.openPicker({
hideTreeRoot: true,
pickableFilter: this.#pickableFilter,
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts
index b1dbeea472..58e708c156 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts
@@ -3,10 +3,11 @@ import { UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '..
import type { UmbDocumentVariantOptionModel } from '../types.js';
import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action';
-import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
+import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
+import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
export class UmbPublishDocumentEntityAction extends UmbEntityActionBase {
constructor(host: UmbControllerHost, args: UmbEntityActionArgs) {
@@ -44,11 +45,18 @@ export class UmbPublishDocumentEntityAction extends UmbEntityActionBase {
}),
);
+ const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
+ const event = new UmbRequestReloadStructureForEntityEvent({
+ unique: this.args.unique,
+ entityType: this.args.entityType,
+ });
+
// If the document has only one variant, we can skip the modal and publish directly:
if (options.length === 1) {
const variantId = UmbVariantId.Create(documentData.variants[0]);
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.publish(this.args.unique, [{ variantId }]);
+ actionEventContext.dispatchEvent(event);
return;
}
@@ -84,6 +92,7 @@ export class UmbPublishDocumentEntityAction extends UmbEntityActionBase {
this.args.unique,
variantIds.map((variantId) => ({ variantId })),
);
+ actionEventContext.dispatchEvent(event);
}
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts
index e20d8c50fc..3de8ac6e96 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts
@@ -2,10 +2,15 @@ import { UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '..
import type { UmbDocumentVariantOptionModel } from '../types.js';
import { UMB_DOCUMENT_UNPUBLISH_MODAL } from '../modals/index.js';
import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
-import { type UmbEntityActionArgs, UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
+import {
+ type UmbEntityActionArgs,
+ UmbEntityActionBase,
+ UmbRequestReloadStructureForEntityEvent,
+} from '@umbraco-cms/backoffice/entity-action';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
+import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase {
constructor(host: UmbControllerHost, args: UmbEntityActionArgs) {
@@ -73,6 +78,14 @@ export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase
if (variantIds.length) {
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.unpublish(this.args.unique, variantIds);
+
+ const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
+ const event = new UmbRequestReloadStructureForEntityEvent({
+ unique: this.args.unique,
+ entityType: this.args.entityType,
+ });
+
+ actionEventContext.dispatchEvent(event);
}
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/publish/publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/publish/publish.action.ts
index b5fd5d1230..5a3ee3e431 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/publish/publish.action.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/publish/publish.action.ts
@@ -8,9 +8,19 @@ import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageCollectionRepository } from '@umbr
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
+import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
+import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
+import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase