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 { async execute() { + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (!entityType) throw new Error('Entity type not found'); + if (unique === undefined) throw new Error('Entity unique not found'); + // If there is only one selection, we can refer to the regular publish entity action: if (this.selection.length === 1) { const action = new UmbPublishDocumentEntityAction(this._host, { @@ -43,6 +53,12 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadChildrenOfEntityEvent({ + entityType, + unique, + }); + // If there is only one language available, we can skip the modal and publish directly: if (options.length === 1) { const localizationController = new UmbLocalizationController(this._host); @@ -62,6 +78,7 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< const variantId = new UmbVariantId(options[0].language.unique, null); const publishingRepository = new UmbDocumentPublishingRepository(this._host); await publishingRepository.unpublish(this.selection[0], [variantId]); + eventContext.dispatchEvent(event); } return; } @@ -98,6 +115,7 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase< unique, variantIds.map((variantId) => ({ variantId })), ); + eventContext.dispatchEvent(event); } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/unpublish/unpublish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/unpublish/unpublish.action.ts index e85694ebed..5c62ca3d49 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/unpublish/unpublish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/unpublish/unpublish.action.ts @@ -8,9 +8,19 @@ import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-act import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; 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 UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBase { async execute() { + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (!entityType) throw new Error('Entity type not found'); + if (unique === undefined) throw new Error('Entity unique not found'); + // If there is only one selection, we can refer to the regular unpublish entity action: if (this.selection.length === 1) { const action = new UmbUnpublishDocumentEntityAction(this._host, { @@ -43,6 +53,12 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadChildrenOfEntityEvent({ + entityType, + unique, + }); + // If there is only one language available, we can skip the modal and unpublish directly: if (options.length === 1) { const localizationController = new UmbLocalizationController(this._host); @@ -62,6 +78,7 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas const variantId = new UmbVariantId(options[0].language.unique, null); const publishingRepository = new UmbDocumentPublishingRepository(this._host); await publishingRepository.unpublish(this.selection[0], [variantId]); + eventContext.dispatchEvent(event); } return; } @@ -95,6 +112,7 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas if (variantIds.length) { for (const unique of this.selection) { await repository.unpublish(unique, variantIds); + eventContext.dispatchEvent(event); } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/rollback/rollback-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/rollback/rollback-modal.element.ts index f38424987f..ace2b3492d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/rollback/rollback-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/rollback/rollback-modal.element.ts @@ -274,7 +274,9 @@ export class UmbRollbackModalElement extends UmbModalBaseElement 0 ? minMax.max : Infinity; } - this._ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes') ?? false; this._startNodeId = config.getValueByAlias('startNodeId'); this._showOpenButton = config.getValueByAlias('showOpenButton') ?? false; } @@ -39,9 +38,6 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl @state() private _showOpenButton?: boolean; - @state() - private _ignoreUserStartNodes?: boolean; - #onChange(event: CustomEvent & { target: UmbInputDocumentElement }) { this.value = event.target.selection.join(','); this.dispatchEvent(new UmbPropertyValueChangeEvent()); @@ -58,7 +54,6 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl .max=${this._max} .startNode=${startNode} .value=${this.value ?? ''} - ?ignoreUserStartNodes=${this._ignoreUserStartNodes} ?showOpenButton=${this._showOpenButton} @change=${this.#onChange}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/index.ts index 2dafba6296..f80023cfbe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/index.ts @@ -1,5 +1,6 @@ export { UmbDocumentDetailRepository, UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from './detail/index.js'; export { UmbDocumentItemRepository, UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from './item/index.js'; export { UmbDocumentPublishingRepository, UMB_DOCUMENT_PUBLISHING_REPOSITORY_ALIAS } from './publishing/index.js'; +export { UmbDocumentPreviewRepository } from './preview/index.js'; export type { UmbDocumentItemModel } from './item/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/preview/document-preview.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/preview/document-preview.repository.ts new file mode 100644 index 0000000000..6b3c1cd87e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/preview/document-preview.repository.ts @@ -0,0 +1,30 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import { PreviewService } from '@umbraco-cms/backoffice/external/backend-api'; +import { tryExecute } from '@umbraco-cms/backoffice/resources'; + +export class UmbDocumentPreviewRepository extends UmbRepositoryBase { + constructor(host: UmbControllerHost) { + super(host); + } + + /** + * Enters preview mode. + * @return {Promise} + * @memberof UmbDocumentPreviewRepository + */ + async enter(): Promise { + await tryExecute(PreviewService.postPreview()); + return; + } + + /** + * Exits preview mode. + * @return {Promise} + * @memberof UmbDocumentPreviewRepository + */ + async exit(): Promise { + await tryExecute(PreviewService.deletePreview()); + return; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/preview/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/preview/index.ts new file mode 100644 index 0000000000..62a286501e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/preview/index.ts @@ -0,0 +1 @@ +export { UmbDocumentPreviewRepository } from './document-preview.repository.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.context.ts new file mode 100644 index 0000000000..63c08e1a0c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.context.ts @@ -0,0 +1,26 @@ +import type { + UmbDocumentTreeItemModel, + UmbDocumentTreeRootItemsRequestArgs, + UmbDocumentTreeRootModel, +} from './types.js'; +import { UMB_CONTENT_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { UmbDefaultTreeContext } from '@umbraco-cms/backoffice/tree'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbDocumentTreeContext extends UmbDefaultTreeContext< + UmbDocumentTreeItemModel, + UmbDocumentTreeRootModel, + UmbDocumentTreeRootItemsRequestArgs +> { + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_CONTENT_PROPERTY_CONTEXT, (context) => { + this.observe(context.dataType, (value) => { + this.updateAdditionalRequestArgs({ dataType: value }); + }); + }); + } +} + +export { UmbDocumentTreeContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.element.ts new file mode 100644 index 0000000000..fd02f52a8d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.element.ts @@ -0,0 +1,14 @@ +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbDefaultTreeElement } from '@umbraco-cms/backoffice/tree'; + +const elementName = 'umb-document-tree'; +@customElement(elementName) +export class UmbDocumentTreeElement extends UmbDefaultTreeElement {} + +export { UmbDocumentTreeElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbDocumentTreeElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts index f3f4c94b89..5a26253cee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts @@ -1,10 +1,10 @@ import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_ROOT_ENTITY_TYPE } from '../entity.js'; -import type { UmbDocumentTreeItemModel } from './types.js'; import type { - UmbTreeAncestorsOfRequestArgs, - UmbTreeChildrenOfRequestArgs, - UmbTreeRootItemsRequestArgs, -} from '@umbraco-cms/backoffice/tree'; + UmbDocumentTreeChildrenOfRequestArgs, + UmbDocumentTreeItemModel, + UmbDocumentTreeRootItemsRequestArgs, +} from './types.js'; +import type { UmbTreeAncestorsOfRequestArgs } from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; import type { DocumentTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api'; @@ -18,7 +18,9 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; */ export class UmbDocumentTreeServerDataSource extends UmbTreeServerDataSourceBase< DocumentTreeItemResponseModel, - UmbDocumentTreeItemModel + UmbDocumentTreeItemModel, + UmbDocumentTreeRootItemsRequestArgs, + UmbDocumentTreeChildrenOfRequestArgs > { /** * Creates an instance of UmbDocumentTreeServerDataSource. @@ -35,17 +37,22 @@ export class UmbDocumentTreeServerDataSource extends UmbTreeServerDataSourceBase } } -const getRootItems = (args: UmbTreeRootItemsRequestArgs) => +const getRootItems = (args: UmbDocumentTreeRootItemsRequestArgs) => // eslint-disable-next-line local-rules/no-direct-api-import - DocumentService.getTreeDocumentRoot({ skip: args.skip, take: args.take }); + DocumentService.getTreeDocumentRoot({ + dataTypeId: args.dataType?.unique, + skip: args.skip, + take: args.take, + }); -const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { +const getChildrenOf = (args: UmbDocumentTreeChildrenOfRequestArgs) => { if (args.parent.unique === null) { return getRootItems(args); } else { // eslint-disable-next-line local-rules/no-direct-api-import return DocumentService.getTreeDocumentChildren({ parentId: args.parent.unique, + dataTypeId: args.dataType?.unique, skip: args.skip, take: args.take, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/manifests.ts index cb3362d82e..78d2219b8d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/manifests.ts @@ -29,9 +29,10 @@ const treeStore: ManifestTreeStore = { const tree: ManifestTree = { type: 'tree', - kind: 'default', alias: UMB_DOCUMENT_TREE_ALIAS, name: 'Document Tree', + api: () => import('./document-tree.context.js'), + element: () => import('./document-tree.element.js'), meta: { repositoryAlias: UMB_DOCUMENT_TREE_REPOSITORY_ALIAS, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts index e97a5168ce..86ca458ffb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts @@ -1,5 +1,10 @@ import type { UmbDocumentEntityType, UmbDocumentRootEntityType } from '../entity.js'; -import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeChildrenOfRequestArgs, + UmbTreeItemModel, + UmbTreeRootItemsRequestArgs, + UmbTreeRootModel, +} from '@umbraco-cms/backoffice/tree'; import type { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; @@ -26,3 +31,15 @@ export interface UmbDocumentTreeItemVariantModel { segment: string | null; state: DocumentVariantStateModel | null; // TODO: make our own enum for this. We might have states for "unsaved changes" etc. } + +export interface UmbDocumentTreeRootItemsRequestArgs extends UmbTreeRootItemsRequestArgs { + dataType?: { + unique: string; + }; +} + +export interface UmbDocumentTreeChildrenOfRequestArgs extends UmbTreeChildrenOfRequestArgs { + dataType?: { + unique: string; + }; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts index 0b7bd4f7e3..3c534d7213 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts @@ -1,8 +1,13 @@ import { UmbDocumentUserPermissionCondition } from '../../user-permissions/document-user-permission.condition.js'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../document-workspace.context-token.js'; import { UMB_USER_PERMISSION_DOCUMENT_UPDATE } from '../../user-permissions/index.js'; import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +// TODO: Investigate how additional preview environments can be supported. [LK:2024-05-16] +// https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api/additional-preview-environments-support +// In v13, they are registered on the server using `SendingContentNotification`, which is no longer available in v14. + export class UmbDocumentSaveAndPreviewWorkspaceAction extends UmbWorkspaceActionBase { constructor(host: UmbControllerHost, args: any) { super(host, args); @@ -24,7 +29,8 @@ export class UmbDocumentSaveAndPreviewWorkspaceAction extends UmbWorkspaceAction } async execute() { - alert('Save and preview'); + const workspaceContext = await this.getContext(UMB_DOCUMENT_WORKSPACE_CONTEXT); + workspaceContext.saveAndPreview(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 658a6b83c0..4bd778fc6c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -24,6 +24,7 @@ import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN, } from '../paths.js'; import { UMB_DOCUMENTS_SECTION_PATH } from '../../paths.js'; +import { UmbDocumentPreviewRepository } from '../repository/preview/index.js'; import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './manifests.js'; import { UmbEntityContext } from '@umbraco-cms/backoffice/entity'; import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant'; @@ -47,8 +48,10 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; import { type Observable, firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; 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'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UmbServerModelValidationContext, @@ -587,7 +590,7 @@ export class UmbDocumentWorkspaceContext // 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, }); @@ -612,6 +615,28 @@ export class UmbDocumentWorkspaceContext } } + async #handleSaveAndPreview() { + const unique = this.getUnique(); + if (!unique) throw new Error('Unique is missing'); + + let culture = UMB_INVARIANT_CULTURE; + + // Save document (the active variant) before previewing. + const { selected } = await this.#determineVariantOptions(); + if (selected.length > 0) { + culture = selected[0]; + const variantId = UmbVariantId.FromString(culture); + const saveData = this.#buildSaveData([variantId]); + await this.#performSaveOrCreate(saveData); + } + + // Tell the server that we're entering preview mode. + await new UmbDocumentPreviewRepository(this).enter(); + + const preview = window.open(`preview?id=${unique}&culture=${culture}`, 'umbpreview'); + preview?.focus(); + } + async #handleSaveAndPublish() { const unique = this.getUnique(); if (!unique) throw new Error('Unique is missing'); @@ -680,6 +705,7 @@ export class UmbDocumentWorkspaceContext }, ); } + async #performSaveAndPublish(variantIds: Array, saveData: UmbDocumentDetailModel): Promise { const unique = this.getUnique(); if (!unique) throw new Error('Unique is missing'); @@ -740,6 +766,10 @@ export class UmbDocumentWorkspaceContext throw new Error('Method not implemented.'); } + public async saveAndPreview(): Promise { + return this.#handleSaveAndPreview(); + } + public async saveAndPublish(): Promise { return this.#handleSaveAndPublish(); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/paths.ts b/src/Umbraco.Web.UI.Client/src/packages/language/paths.ts new file mode 100644 index 0000000000..108e46166d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/paths.ts @@ -0,0 +1,16 @@ +import { UMB_LANGUAGE_ENTITY_TYPE } from './entity.js'; +import { UMB_SETTINGS_SECTION_PATHNAME } from '@umbraco-cms/backoffice/settings'; +import { UmbPathPattern } from '@umbraco-cms/backoffice/router'; +import { UMB_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/workspace'; + +export const UMB_LANGUAGE_WORKSPACE_PATH = UMB_WORKSPACE_PATH_PATTERN.generateAbsolute({ + sectionName: UMB_SETTINGS_SECTION_PATHNAME, + entityType: UMB_LANGUAGE_ENTITY_TYPE, +}); + +export const UMB_CREATE_LANGUAGE_WORKSPACE_PATH_PATTERN = new UmbPathPattern('create', UMB_LANGUAGE_WORKSPACE_PATH); + +export const UMB_EDIT_LANGUAGE_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ unique: string }>( + 'edit/:unique', + UMB_LANGUAGE_WORKSPACE_PATH, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.modal-token.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.modal-token.ts new file mode 100644 index 0000000000..1e82ee6dd6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/language-workspace.modal-token.ts @@ -0,0 +1,15 @@ +import type { UmbLanguageDetailModel } from '../../types.js'; +import type { UmbWorkspaceModalData, UmbWorkspaceModalValue } from '@umbraco-cms/backoffice/modal'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export const UMB_LANGUAGE_WORKSPACE_MODAL = new UmbModalToken< + UmbWorkspaceModalData, + UmbWorkspaceModalValue +>('Umb.Modal.Workspace', { + modal: { + type: 'sidebar', + size: 'large', + }, + data: { entityType: 'language', preset: {} }, + // Recast the type, so the entityType data prop is not required: +}) as UmbModalToken, 'entityType'>, UmbWorkspaceModalValue>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/index.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/index.ts index 3d76f338dd..5e55e78ec7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/index.ts @@ -1 +1,3 @@ export * from './repository/index.js'; +export * from './components/donut-chart/donut-chart.element.js'; +export * from './components/donut-chart/donut-slice.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts index ff0642ff09..446827dd3f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts @@ -18,8 +18,10 @@ import type { import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; 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 = UmbMediaTypeDetailModel; export class UmbMediaTypeWorkspaceContext @@ -214,7 +216,7 @@ export class UmbMediaTypeWorkspaceContext await this.structure.create(parent.unique); 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/media/media/collection/action/create-media-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts index 03536dcebb..c4efef226a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts @@ -1,8 +1,10 @@ +import { UMB_MEDIA_COLLECTION_CONTEXT } from '../media-collection.context-token.js'; +import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../../workspace/index.js'; +import { UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN } from '../../paths.js'; +import { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js'; import { html, customElement, property, state, map } from '@umbraco-cms/backoffice/external/lit'; import { UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; -import { UMB_MEDIA_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/media'; import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import type { ManifestCollectionAction } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; @@ -15,6 +17,9 @@ export class UmbCreateMediaCollectionActionElement extends UmbLitElement { @state() private _createMediaPath = ''; + @state() + private _currentView?: string; + @state() private _mediaUnique?: string; @@ -25,7 +30,7 @@ export class UmbCreateMediaCollectionActionElement extends UmbLitElement { private _popoverOpen = false; @state() - private _useInfiniteEditor = false; + private _rootPathName?: string; @property({ attribute: false }) manifest?: ManifestCollectionAction; @@ -53,9 +58,12 @@ export class UmbCreateMediaCollectionActionElement extends UmbLitElement { }); }); - this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => { - this.observe(collectionContext.filter, (filter) => { - this._useInfiniteEditor = filter.useInfiniteEditor == true; + this.consumeContext(UMB_MEDIA_COLLECTION_CONTEXT, (collectionContext) => { + this.observe(collectionContext.view.currentView, (currentView) => { + this._currentView = currentView?.meta.pathName; + }); + this.observe(collectionContext.view.rootPathName, (rootPathName) => { + this._rootPathName = rootPathName; }); }); } @@ -78,15 +86,15 @@ export class UmbCreateMediaCollectionActionElement extends UmbLitElement { this._popoverOpen = event.newState === 'open'; } - #getCreateUrl(mediaType: UmbAllowedMediaTypeModel) { - // TODO: [LK] I need help with this. I don't know what the infinity editor URL should be. - - const mediaEntityType = 'media-root'; // TODO: this should be dynamic - return this._useInfiniteEditor - ? `${this._createMediaPath}create/${this._mediaUnique ?? 'null'}/${mediaType.unique}` - : `section/media/workspace/media/create/parent/${mediaEntityType}/${this._mediaUnique ?? 'null'}/${ - mediaType.unique - }`; + #getCreateUrl(item: UmbAllowedMediaTypeModel) { + return ( + this._createMediaPath.replace(`${this._rootPathName}`, `${this._rootPathName}/${this._currentView}`) + + UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN.generateLocal({ + parentEntityType: this._mediaUnique ? UMB_MEDIA_ENTITY_TYPE : UMB_MEDIA_ROOT_ENTITY_TYPE, + parentUnique: this._mediaUnique ?? 'null', + documentTypeUnique: item.unique, + }) + ); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection-toolbar.element.ts index a94a8e7ac0..45b8250678 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection-toolbar.element.ts @@ -1,7 +1,7 @@ import type { UmbMediaCollectionContext } from './media-collection.context.js'; +import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js'; import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; @customElement('umb-media-collection-toolbar') export class UmbMediaCollectionToolbarElement extends UmbLitElement { @@ -13,8 +13,8 @@ export class UmbMediaCollectionToolbarElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance as UmbMediaCollectionContext; + this.consumeContext(UMB_MEDIA_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context-token.ts new file mode 100644 index 0000000000..eb61212cba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context-token.ts @@ -0,0 +1,4 @@ +import type { UmbMediaCollectionContext } from './media-collection.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_MEDIA_COLLECTION_CONTEXT = new UmbContextToken('UmbCollectionContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts index ee33b64eea..dc58abba08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts @@ -1,9 +1,10 @@ -import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from './types.js'; import { UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS } from './views/index.js'; +import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< UmbMediaCollectionItemModel, @@ -21,7 +22,10 @@ export class UmbMediaCollectionContext extends UmbDefaultCollectionContext< this.observe(this.items, async (items) => { if (!items?.length) return; - const { data } = await this.#imagingRepository.requestResizedItems(items.map((m) => m.unique)); + const { data } = await this.#imagingRepository.requestResizedItems( + items.map((m) => m.unique), + { height: 400, width: 400, mode: ImageCropModeModel.MIN }, + ); this.#thumbnailItems.setValue( items.map((item) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts index 01a482a323..ff7478317c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts @@ -1,6 +1,7 @@ import type { UmbMediaCollectionContext } from './media-collection.context.js'; +import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js'; import { customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit'; -import { UMB_COLLECTION_CONTEXT, UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; +import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import type { UmbProgressEvent } from '@umbraco-cms/backoffice/event'; import './media-collection-toolbar.element.js'; @@ -14,8 +15,8 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#mediaCollection = instance as UmbMediaCollectionContext; + this.consumeContext(UMB_MEDIA_COLLECTION_CONTEXT, (instance) => { + this.#mediaCollection = instance; }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts index d8e8f2d220..db2b147848 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts @@ -1,10 +1,10 @@ import { UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; import type { UmbMediaCollectionItemModel } from '../../types.js'; import type { UmbMediaCollectionContext } from '../../media-collection.context.js'; +import { UMB_MEDIA_COLLECTION_CONTEXT } from '../../media-collection.context-token.js'; import { css, customElement, html, nothing, repeat, 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'; @customElement('umb-media-grid-collection-view') @@ -25,8 +25,8 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => { - this.#collectionContext = collectionContext as UmbMediaCollectionContext; + this.consumeContext(UMB_MEDIA_COLLECTION_CONTEXT, (collectionContext) => { + this.#collectionContext = collectionContext; this.#observeCollectionContext(); }); @@ -116,7 +116,6 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { } #renderItem(item: UmbMediaCollectionItemModel) { - // TODO: Fix the file extension when media items have a file extension. [?] return html` this.#onOpen(event, item.unique)} @selected=${() => this.#onSelect(item)} @deselected=${() => this.#onDeselect(item)} - class="media-item" - file-ext="${item.icon}"> + class="media-item"> ${item.url ? html`${item.name}` : html``} @@ -155,8 +153,15 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { grid-auto-rows: 200px; gap: var(--uui-size-space-5); } + + img { + background-image: url('data:image/svg+xml;charset=utf-8,'); + background-size: 10px 10px; + background-repeat: repeat; + } + umb-icon { - font-size: var(--uui-size-24); + font-size: var(--uui-size-8); } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts index 559c9f62df..5e69fa26ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/table/media-table-collection-view.element.ts @@ -1,10 +1,10 @@ import { UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js'; import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from '../../types.js'; +import { UMB_MEDIA_COLLECTION_CONTEXT } from '../../media-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 type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; import type { UmbTableColumn, @@ -60,7 +60,7 @@ export class UmbMediaTableCollectionViewElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => { + this.consumeContext(UMB_MEDIA_COLLECTION_CONTEXT, (collectionContext) => { this.#collectionContext = collectionContext; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts index e23da648b4..ce4783482f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-focus-setter.element.ts @@ -5,20 +5,15 @@ import { LitElement, css, html, nothing, customElement, property, query } from ' @customElement('umb-image-cropper-focus-setter') export class UmbImageCropperFocusSetterElement extends LitElement { - @query('#image') imageElement?: HTMLImageElement; + @query('#image') imageElement!: HTMLImageElement; @query('#wrapper') wrapperElement?: HTMLImageElement; - @query('#focal-point') focalPointElement?: HTMLImageElement; + @query('#focal-point') focalPointElement!: HTMLImageElement; @property({ type: String }) src?: string; @property({ attribute: false }) focalPoint: UmbImageCropperFocalPoint = { left: 0.5, top: 0.5 }; #DOT_RADIUS = 6 as const; - connectedCallback() { - super.connectedCallback(); - this.#addEventListeners(); - } - disconnectedCallback() { super.disconnectedCallback(); this.#removeEventListeners(); @@ -33,33 +28,46 @@ export class UmbImageCropperFocusSetterElement extends LitElement { } } + protected update(changedProperties: PropertyValueMap | Map): void { + super.update(changedProperties); + + if (changedProperties.has('src')) { + if (this.src) { + this.#initializeImage(); + } + } + } + protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { super.firstUpdated(_changedProperties); this.style.setProperty('--dot-radius', `${this.#DOT_RADIUS}px`); + } - if (this.focalPointElement) { - this.focalPointElement.style.left = `calc(${this.focalPoint.left * 100}% - ${this.#DOT_RADIUS}px)`; - this.focalPointElement.style.top = `calc(${this.focalPoint.top * 100}% - ${this.#DOT_RADIUS}px)`; - } - if (this.imageElement) { - this.imageElement.onload = () => { - if (!this.imageElement || !this.wrapperElement) return; - const imageAspectRatio = this.imageElement.naturalWidth / this.imageElement.naturalHeight; - const hostRect = this.getBoundingClientRect(); - const image = this.imageElement.getBoundingClientRect(); + async #initializeImage() { + await this.updateComplete; // Wait for the @query to be resolved - if (image.width > hostRect.width) { - this.imageElement.style.width = '100%'; - } - if (image.height > hostRect.height) { - this.imageElement.style.height = '100%'; - } + this.focalPointElement.style.left = `calc(${this.focalPoint.left * 100}% - ${this.#DOT_RADIUS}px)`; + this.focalPointElement.style.top = `calc(${this.focalPoint.top * 100}% - ${this.#DOT_RADIUS}px)`; - this.imageElement.style.aspectRatio = `${imageAspectRatio}`; - this.wrapperElement.style.aspectRatio = `${imageAspectRatio}`; - }; - } + this.imageElement.onload = () => { + if (!this.imageElement || !this.wrapperElement) return; + const imageAspectRatio = this.imageElement.naturalWidth / this.imageElement.naturalHeight; + const hostRect = this.getBoundingClientRect(); + const image = this.imageElement.getBoundingClientRect(); + + if (image.width > hostRect.width) { + this.imageElement.style.width = '100%'; + } + if (image.height > hostRect.height) { + this.imageElement.style.height = '100%'; + } + + this.imageElement.style.aspectRatio = `${imageAspectRatio}`; + this.wrapperElement.style.aspectRatio = `${imageAspectRatio}`; + }; + + this.#addEventListeners(); } async #addEventListeners() { @@ -134,6 +142,7 @@ export class UmbImageCropperFocusSetterElement extends LitElement { } /* Wrapper is used to make the focal point position responsive to the image size */ #wrapper { + overflow: hidden; position: relative; display: flex; margin: auto; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts index ffda92c71c..7d94c9740e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-preview.element.ts @@ -33,11 +33,7 @@ export class UmbImageCropperPreviewElement extends LitElement { if (!this.crop) return; await this.updateComplete; // Wait for the @query to be resolved - - if (!this.imageElement.complete) { - // Wait for the image to load - await new Promise((resolve) => (this.imageElement.onload = () => resolve(this.imageElement))); - } + await new Promise((resolve) => (this.imageElement.onload = () => resolve(this.imageElement))); const container = this.imageContainerElement.getBoundingClientRect(); const cropAspectRatio = this.crop.width / this.crop.height; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts index 6599c9a769..807193942c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts @@ -376,6 +376,7 @@ export class UmbImageCropperElement extends LitElement { #image { display: block; position: absolute; + user-select: none; } #slider { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts index d5e6900e27..e5dbf38b11 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts @@ -56,7 +56,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { this.value = assignToFrozenObject(this.value, { temporaryFileId: unique }); - this.#manager?.uploadOne({ unique, file }); + this.#manager?.uploadOne({ temporaryUnique: unique, file }); this.dispatchEvent(new UmbChangeEvent()); } @@ -68,8 +68,9 @@ export class UmbInputImageCropperElement extends UmbLitElement { #onRemove = () => { this.value = assignToFrozenObject(this.value, { src: '', temporaryFileId: null }); - if (!this.fileUnique) return; - this.#manager?.removeOne(this.fileUnique); + if (this.fileUnique) { + this.#manager?.removeOne(this.fileUnique); + } this.fileUnique = undefined; this.file = undefined; @@ -114,7 +115,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { const value = (e.target as UmbInputImageCropperFieldElement).value; if (!value) { - this.value = { src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 } }; + this.value = { src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, temporaryFileId: null }; this.dispatchEvent(new UmbChangeEvent()); return; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts index ae6fa4a43f..fc06cf3dc2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts @@ -1,21 +1,48 @@ +import { + UMB_MEDIA_PICKER_MODAL, + type UmbMediaPickerModalData, + type UmbMediaPickerModalValue, + type UmbMediaCardItemModel, +} from '../../modals/index.js'; import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import type { UmbMediaItemModel } from '../../repository/item/types.js'; -import type { UmbMediaTreeItemModel } from '../../tree/index.js'; -import { UMB_MEDIA_TREE_PICKER_MODAL } from '../../tree/index.js'; -import type { - UmbMediaTreePickerModalData, - UmbMediaTreePickerModalValue, -} from '../../tree/media-tree-picker-modal.token.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; +import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; export class UmbMediaPickerContext extends UmbPickerInputContext< UmbMediaItemModel, - UmbMediaTreeItemModel, - UmbMediaTreePickerModalData, - UmbMediaTreePickerModalValue + UmbMediaItemModel, + UmbMediaPickerModalData, + UmbMediaPickerModalValue > { + #imagingRepository: UmbImagingRepository; + + #cardItems = new UmbArrayState([], (x) => x.unique); + readonly cardItems = this.#cardItems.asObservable(); + constructor(host: UmbControllerHost) { - super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL); + super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_PICKER_MODAL); + this.#imagingRepository = new UmbImagingRepository(host); + + this.observe(this.selectedItems, async (selectedItems) => { + if (!selectedItems?.length) { + this.#cardItems.setValue([]); + return; + } + const { data } = await this.#imagingRepository.requestResizedItems( + selectedItems.map((x) => x.unique), + { height: 400, width: 400, mode: ImageCropModeModel.MIN }, + ); + + this.#cardItems.setValue( + selectedItems.map((item) => { + const url = data?.find((x) => x.unique === item.unique)?.url; + return { ...item, url }; + }), + ); + }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index bfcea58eb4..b86839a893 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -1,3 +1,4 @@ +import type { UmbMediaCardItemModel } from '../../modals/index.js'; import type { UmbMediaItemModel } from '../../repository/index.js'; import { UmbMediaPickerContext } from './input-media.context.js'; import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit'; @@ -20,9 +21,10 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') identifier: 'Umb.SorterIdentifier.InputMedia', itemSelector: 'uui-card-media', containerSelector: '.container', + /** TODO: This component probably needs some grid-like logic for resolve placement... [LI] */ + resolvePlacement: () => false, onChange: ({ model }) => { this.selection = model; - this.dispatchEvent(new UmbChangeEvent()); }, }); @@ -87,8 +89,11 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') @property({ type: Boolean }) showOpenButton?: boolean; + @property({ type: String }) + startNode = ''; + @property({ type: Boolean }) - ignoreUserStartNodes?: boolean; + multiple = false; @property() public set value(idsString: string) { @@ -100,10 +105,10 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') } @state() - private _editMediaPath = ''; + protected editMediaPath = ''; @state() - private _items?: Array; + protected items?: Array; #pickerContext = new UmbMediaPickerContext(this); @@ -116,11 +121,13 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') return { data: { entityType: 'media', preset: {} } }; }) .observeRouteBuilder((routeBuilder) => { - this._editMediaPath = routeBuilder({}); + this.editMediaPath = routeBuilder({}); }); this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(','))); - this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + this.observe(this.#pickerContext.cardItems, (cardItems) => { + this.items = cardItems; + }); this.addValidator( 'rangeUnderflow', @@ -147,28 +154,32 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') }; #openPicker() { - // TODO: Configure the media picker, with `ignoreUserStartNodes` [LK] this.#pickerContext.openPicker({ - hideTreeRoot: true, + multiple: this.multiple, + startNode: this.startNode, pickableFilter: this.#pickableFilter, }); } + protected onRemove(item: UmbMediaCardItemModel) { + this.#pickerContext.requestRemoveItem(item.unique); + } + render() { return html`
${this.#renderItems()} ${this.#renderAddButton()}
`; } #renderItems() { - if (!this._items) return; + if (!this.items?.length) return; return html`${repeat( - this._items, + this.items, (item) => item.unique, - (item) => this.#renderItem(item), + (item) => this.renderItem(item), )}`; } #renderAddButton() { - if (this._items && this.max && this._items.length >= this.max) return; + if ((this.items && this.max && this.items.length >= this.max) || (this.items?.length && !this.multiple)) return; return html` - ${this.#renderIsTrashed(item)} + href="${this.editMediaPath}edit/${item.unique}"> + ${item.url + ? html`${item.name}` + : html``} + ${this.renderIsTrashed(item)} - ${this.#renderOpenButton(item)} - - - this.#pickerContext.requestRemoveItem(item.unique)} - label="Remove media ${item.name}"> + label=${this.localize.term('general_remove')} + look="secondary" + @click=${() => this.onRemove(item)}> @@ -204,7 +214,7 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') `; } - #renderIsTrashed(item: UmbMediaItemModel) { + protected renderIsTrashed(item: UmbMediaCardItemModel) { if (!item.isTrashed) return; return html` @@ -213,25 +223,16 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') `; } - #renderOpenButton(item: UmbMediaItemModel) { - if (!this.showOpenButton) return; - return html` - - - - `; - } - static styles = [ css` + :host { + position: relative; + } .container { display: grid; gap: var(--uui-size-space-3); - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - grid-template-rows: repeat(auto-fill, minmax(160px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + grid-auto-rows: 150px; } #btn-add { @@ -244,6 +245,10 @@ export class UmbInputMediaElement extends UUIFormControlMixin(UmbLitElement, '') margin: 0 auto; } + uui-card-media umb-icon { + font-size: var(--uui-size-8); + } + uui-card-media[drag-placeholder] { opacity: 0.2; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts index c96203b8c0..191e7e08c5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -15,7 +15,6 @@ import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; export interface UmbUploadableFileModel extends UmbTemporaryFileModel { unique: string; - file: File; mediaTypeUnique: string; } @@ -38,7 +37,10 @@ export class UmbDropzoneManager extends UmbControllerBase { #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); #mediaDetailRepository = new UmbMediaDetailRepository(this); - #completed = new UmbArrayState([], (upload) => upload.unique); + #completed = new UmbArrayState( + [], + (upload) => upload.temporaryUnique, + ); public readonly completed = this.#completed.asObservable(); constructor(host: UmbControllerHost) { @@ -56,7 +58,7 @@ export class UmbDropzoneManager extends UmbControllerBase { const temporaryFiles: Array = []; for (const file of files) { - const uploaded = await this.#tempFileManager.uploadOne({ unique: UmbId.new(), file }); + const uploaded = await this.#tempFileManager.uploadOne({ temporaryUnique: UmbId.new(), file }); this.#completed.setValue([...this.#completed.getValue(), uploaded]); temporaryFiles.push(uploaded); } @@ -107,7 +109,12 @@ export class UmbDropzoneManager extends UmbControllerBase { // Since we are uploading multiple files, we will pick first allowed option. // Consider a way we can handle this differently in the future to let the user choose. Maybe a list of all files with an allowed media type dropdown? const mediaType = options[0]; - uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique }); + uploadableFiles.push({ + temporaryUnique: UmbId.new(), + file, + mediaTypeUnique: mediaType.unique, + unique: UmbId.new(), + }); } notAllowedFiles.forEach((file) => { @@ -142,6 +149,7 @@ export class UmbDropzoneManager extends UmbControllerBase { // Only one allowed option, upload file using that option. const uploadableFile: UmbUploadableFileModel = { unique: UmbId.new(), + temporaryUnique: UmbId.new(), file, mediaTypeUnique: mediaTypes[0].unique, }; @@ -156,6 +164,7 @@ export class UmbDropzoneManager extends UmbControllerBase { const uploadableFile: UmbUploadableFileModel = { unique: UmbId.new(), + temporaryUnique: UmbId.new(), file, mediaTypeUnique: mediaType.unique, }; @@ -211,6 +220,7 @@ export class UmbDropzoneManager extends UmbControllerBase { if (upload.status === TemporaryFileStatus.SUCCESS) { // Upload successful. Create media item. const preset: Partial = { + unique: file.unique, mediaType: { unique: upload.mediaTypeUnique, collection: null, @@ -227,7 +237,7 @@ export class UmbDropzoneManager extends UmbControllerBase { values: [ { alias: 'umbracoFile', - value: { temporaryFileId: upload.unique }, + value: { temporaryFileId: upload.temporaryUnique }, culture: null, segment: null, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts index d88040d01a..726347040b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -1,14 +1,32 @@ -import { UmbDropzoneManager } from './dropzone-manager.class.js'; -import { UmbChangeEvent, UmbProgressEvent } from '@umbraco-cms/backoffice/event'; +import { UmbDropzoneManager, type UmbUploadableFileModel } from './dropzone-manager.class.js'; +import { UmbProgressEvent } from '@umbraco-cms/backoffice/event'; import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file'; @customElement('umb-dropzone') export class UmbDropzoneElement extends UmbLitElement { @property({ attribute: false }) parentUnique: string | null = null; + @property({ type: Boolean }) + multiple: boolean = true; + + @property({ type: Boolean }) + createAsTemporary: boolean = false; + + @property({ type: Array, attribute: false }) + accept: Array = []; + + //TODO: logic to disable the dropzone? + + #files: Array = []; + + public getFiles() { + return this.#files; + } + public browse() { const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; return element.browse(); @@ -28,7 +46,9 @@ export class UmbDropzoneElement extends UmbLitElement { document.removeEventListener('drop', this.#handleDrop.bind(this)); } - #handleDragEnter() { + #handleDragEnter(e: DragEvent) { + // Avoid collision with UmbSorterController + if (!e.dataTransfer?.types?.length) return; this.toggleAttribute('dragging', true); } @@ -57,23 +77,28 @@ export class UmbDropzoneElement extends UmbLitElement { this.dispatchEvent(new UmbProgressEvent(progress)); if (completed.length === files.length) { - this.dispatchEvent(new UmbChangeEvent()); + this.#files = completed; + this.dispatchEvent(new CustomEvent('change', { detail: { completed } })); dropzoneManager.destroy(); } }, '_observeCompleted', ); //TODO Create some placeholder items while files are being uploaded? Could update them as they get completed. - await dropzoneManager.createFilesAsMedia(files, this.parentUnique); + if (this.createAsTemporary) { + await dropzoneManager.createFilesAsTemporary(files); + } else { + await dropzoneManager.createFilesAsMedia(files, this.parentUnique); + } } render() { return html``; + label="${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}">`; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity.ts index 1df9b8aa2e..c7a5df2f81 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity.ts @@ -3,3 +3,5 @@ export const UMB_MEDIA_ROOT_ENTITY_TYPE = 'media-root'; export type UmbMediaEntityType = typeof UMB_MEDIA_ENTITY_TYPE; export type UmbMediaRootEntityType = typeof UMB_MEDIA_ROOT_ENTITY_TYPE; + +export type UmbMediaEntityTypeUnion = UmbMediaEntityType | UmbMediaRootEntityType; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index c76bd46ae8..1865bc7f66 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,23 +1,39 @@ -import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; -import { type UmbMediaItemModel, UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js'; +import { UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js'; +import type { UmbDropzoneElement } from '../../dropzone/dropzone.element.js'; +import type { UmbMediaItemModel } from '../../repository/index.js'; import type { UmbMediaCardItemModel, UmbMediaPathModel } from './types.js'; import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; +import { css, html, customElement, state, repeat, ifDefined, query } from '@umbraco-cms/backoffice/external/lit'; +import { debounce } from '@umbraco-cms/backoffice/utils'; +import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbImagingRepository } from '@umbraco-cms/backoffice/imaging'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_CONTENT_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE }; @customElement('umb-media-picker-modal') -export class UmbMediaPickerModalElement extends UmbModalBaseElement { +export class UmbMediaPickerModalElement extends UmbModalBaseElement< + UmbMediaPickerModalData, + UmbMediaPickerModalValue +> { #mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure #mediaUrlRepository = new UmbMediaUrlRepository(this); // used to get urls #mediaItemRepository = new UmbMediaItemRepository(this); // used to search #imagingRepository = new UmbImagingRepository(this); // used to get image renditions + #dataType?: { unique: string }; + + @state() + private _filter: (item: UmbMediaCardItemModel) => boolean = () => true; + + @state() + private _selectableFilter: (item: UmbMediaCardItemModel) => boolean = () => true; + #mediaItemsCurrentFolder: Array = []; @state() @@ -32,9 +48,25 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement { + this.observe(context.dataType, (dataType) => { + this.#dataType = dataType; + }); + }); + } + async connectedCallback(): Promise { super.connectedCallback(); + if (this.data?.filter) this._filter = this.data?.filter; + if (this.data?.pickableFilter) this._selectableFilter = this.data?.pickableFilter; + if (this.data?.startNode) { const { data } = await this.#mediaItemRepository.requestItems([this.data.startNode]); @@ -52,6 +84,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement): Promise> { if (!items.length) return []; - const { data } = await this.#imagingRepository.requestResizedItems(items.map((item) => item.unique)); + const { data } = await this.#imagingRepository.requestResizedItems( + items.map((item) => item.unique), + { height: 400, width: 400, mode: ImageCropModeModel.MIN }, + ); - return items.map((item): UmbMediaCardItemModel => { - const url = data?.find((media) => media.unique === item.unique)?.url; - return { name: item.name, unique: item.unique, url, icon: item.mediaType.icon, entityType: item.entityType }; - }); + return items + .map((item): UmbMediaCardItemModel => { + const url = data?.find((media) => media.unique === item.unique)?.url; + return { ...item, url }; + }) + .filter((item) => this._filter(item)); } #onOpen(item: UmbMediaCardItemModel) { @@ -118,9 +156,13 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement found.isTrashed === false)); } + #debouncedSearch = debounce(() => { + this.#filterMediaItems(); + }, 500); + #onSearch(e: UUIInputEvent) { this._searchQuery = (e.target.value as string).toLocaleLowerCase(); - this.#filterMediaItems(); + this.#debouncedSearch(); } #onPathChange(e: CustomEvent) { @@ -165,37 +207,43 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement - - `; + `; } - // Where should this be placed, without it looking terrible? - // (this._searchOnlyThisFolder = !this._searchOnlyThisFolder)} label=${this.localize.term('general_excludeFromSubFolders')}> - #renderCard(item: UmbMediaCardItemModel) { + const disabled = !this._selectableFilter(item); return html` this.#onOpen(item)} @selected=${() => this.#onSelected(item)} @deselected=${() => this.#onDeselected(item)} ?selected=${this.value?.selection?.find((value) => value === item.unique)} - selectable> + ?selectable=${!disabled}> ${item.url ? html`${ifDefined(item.name)}` - : html``} + : html``} `; } @@ -230,13 +278,13 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement { startNode?: string | null; multiple?: boolean; + pickableFilter?: (item: ItemType) => boolean; + filter?: (item: ItemType) => boolean; } export type UmbMediaPickerModalValue = { selection: string[]; }; -export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken( +export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken, UmbMediaPickerModalValue>( 'Umb.Modal.MediaPicker', { modal: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts index 768968bff8..9209e33c72 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts @@ -1,12 +1,8 @@ -import type { UmbMediaEntityType } from '../../entity.js'; +import type { UmbMediaItemModel } from '../../repository/index.js'; import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; -export interface UmbMediaCardItemModel { - name: string; - unique: string; - entityType: UmbMediaEntityType; +export interface UmbMediaCardItemModel extends UmbMediaItemModel { url?: string; - icon?: string; } export interface UmbMediaPathModel extends UmbEntityModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts index d5eb6fdfd5..2dec2a0786 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/paths.ts @@ -1,3 +1,17 @@ +import { UMB_MEDIA_SECTION_PATHNAME } from '../paths.js'; +import { UMB_MEDIA_ENTITY_TYPE, type UmbMediaEntityTypeUnion } from './entity.js'; +import { UMB_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/workspace'; import { UmbPathPattern } from '@umbraco-cms/backoffice/router'; +export const UMB_MEDIA_WORKSPACE_PATH = UMB_WORKSPACE_PATH_PATTERN.generateAbsolute({ + sectionName: UMB_MEDIA_SECTION_PATHNAME, + entityType: UMB_MEDIA_ENTITY_TYPE, +}); + +export const UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ + parentEntityType: UmbMediaEntityTypeUnion; + parentUnique?: string | null; + documentTypeUnique: string; +}>('create/parent/:parentEntityType/:parentUnique/:documentTypeUnique', UMB_MEDIA_WORKSPACE_PATH); + export const UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ unique: string }>('edit/:unique'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts index f726bbae26..0c546344a3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-cropper/property-editor-ui-image-cropper.element.ts @@ -15,6 +15,7 @@ import { export class UmbPropertyEditorUIImageCropperElement extends UmbLitElement implements UmbPropertyEditorUiElement { @property({ attribute: false }) value: UmbImageCropperPropertyEditorValue = { + temporaryFileId: null, src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, @@ -28,6 +29,7 @@ export class UmbPropertyEditorUIImageCropperElement extends UmbLitElement implem if (changedProperties.has('value')) { if (!this.value) { this.value = { + temporaryFileId: null, src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-crops-configuration/property-editor-ui-image-crops-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-crops-configuration/property-editor-ui-image-crops-configuration.element.ts index bbccbeac1f..524f90c410 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-crops-configuration/property-editor-ui-image-crops-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/image-crops-configuration/property-editor-ui-image-crops-configuration.element.ts @@ -1,8 +1,9 @@ -import { html, customElement, property, css, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, css, repeat, state, query } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import { generateAlias } from '@umbraco-cms/backoffice/utils'; export type UmbCrop = { label: string; @@ -19,6 +20,9 @@ export class UmbPropertyEditorUIImageCropsConfigurationElement extends UmbLitElement implements UmbPropertyEditorUiElement { + @query('#label') + private _labelInput!: HTMLInputElement; + //TODO MAKE TYPE @property({ attribute: false }) value: UmbCrop[] = []; @@ -26,6 +30,8 @@ export class UmbPropertyEditorUIImageCropsConfigurationElement @state() editCropAlias = ''; + #oldInputValue = ''; + #onRemove(alias: string) { this.value = [...this.value.filter((item) => item.alias !== alias)]; this.dispatchEvent(new UmbPropertyValueChangeEvent()); @@ -92,6 +98,7 @@ export class UmbPropertyEditorUIImageCropsConfigurationElement this.dispatchEvent(new UmbPropertyValueChangeEvent()); form.reset(); + this._labelInput.focus(); } #renderActions() { @@ -101,6 +108,24 @@ export class UmbPropertyEditorUIImageCropsConfigurationElement : html``; } + #onLabelInput() { + const value = this._labelInput.value ?? ''; + + const aliasValue = generateAlias(value); + + const alias = this.shadowRoot?.querySelector('#alias') as HTMLInputElement; + + if (!alias) return; + + const oldAliasValue = generateAlias(this.#oldInputValue); + + if (alias.value === oldAliasValue || !alias.value) { + alias.value = aliasValue; + } + + this.#oldInputValue = value; + } + render() { if (!this.value) this.value = []; @@ -109,7 +134,14 @@ export class UmbPropertyEditorUIImageCropsConfigurationElement
Label - +
Alias diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-entity-picker/property-editor-ui-media-entity-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-entity-picker/property-editor-ui-media-entity-picker.element.ts index 2653a68a76..6e6bbafd45 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-entity-picker/property-editor-ui-media-entity-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-entity-picker/property-editor-ui-media-entity-picker.element.ts @@ -33,7 +33,7 @@ export class UmbPropertyEditorUIMediaEntityPickerElement extends UmbLitElement i return undefined; } - #onChange(event: { target: UmbInputMediaElement }) { + #onChange(event: CustomEvent & { target: UmbInputMediaElement }) { this.value = event.target.selection?.join(',') ?? null; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/index.ts new file mode 100644 index 0000000000..9044e60dbf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/index.ts @@ -0,0 +1 @@ +export * from './input-rich-media/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/input-rich-media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/input-rich-media/index.ts new file mode 100644 index 0000000000..0c7c12b80e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/input-rich-media/index.ts @@ -0,0 +1 @@ +export * from './input-rich-media.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/input-rich-media/input-rich-media.element.ts new file mode 100644 index 0000000000..b933120f23 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/components/input-rich-media/input-rich-media.element.ts @@ -0,0 +1,69 @@ +import type { UmbCropModel } from '../../index.js'; +import type { UmbMediaCardItemModel } from '../../../../modals/index.js'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { customElement, html, ifDefined, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; +import type { UmbUploadableFileModel } from '@umbraco-cms/backoffice/media'; + +const elementName = 'umb-input-rich-media'; +@customElement(elementName) +export class UmbInputRichMediaElement extends UmbInputMediaElement { + @property({ type: Boolean }) + focalPointEnabled = false; + + @property({ type: Array }) + crops?: Array; + + async #onUploadCompleted(e: CustomEvent) { + const completed = e.detail?.completed as Array; + const uploaded = completed.map((file) => file.unique); + + this.selection = [...this.selection, ...uploaded]; + this.dispatchEvent(new UmbChangeEvent()); + } + + render() { + return html`${this.#renderDropzone()} ${super.render()}`; + } + + #renderDropzone() { + if (this.items && this.items.length >= this.max) return; + return html``; + } + + protected renderItem(item: UmbMediaCardItemModel) { + return html` + { + alert('open media crops modal'); + }}> + ${item.url + ? html`${item.name}` + : html``} + ${this.renderIsTrashed(item)} + + + + + this.onRemove(item)}> + + + + + `; + } +} + +export { UmbInputRichMediaElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbInputRichMediaElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/index.ts index 54a8835b52..e8c480b60c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/index.ts @@ -5,5 +5,12 @@ export type UmbMediaPickerPropertyValue = { mediaKey: string; mediaTypeAlias: string; focalPoint: { left: number; top: number } | null; - crops: Array<{ alias: string; width: number; height: number }>; + crops: Array; }; + +export interface UmbCropModel { + label: string; + alias: string; + width: number; + height: number; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts index f6e7749aab..6a98cca0e5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts @@ -1,18 +1,19 @@ -import type { UmbInputMediaElement } from '../../components/input-media/input-media.element.js'; -import '../../components/input-media/input-media.element.js'; -import type { UmbMediaPickerPropertyValue } from './index.js'; -import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { - UmbPropertyValueChangeEvent, - type UmbPropertyEditorConfigCollection, -} from '@umbraco-cms/backoffice/property-editor'; -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbInputRichMediaElement } from './components/input-rich-media/input-rich-media.element.js'; +import type { UmbCropModel, UmbMediaPickerPropertyValue } from './index.js'; +import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; + +import './components/input-rich-media/input-rich-media.element.js'; /** * @element umb-property-editor-ui-media-picker */ + @customElement('umb-property-editor-ui-media-picker') export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { @property({ attribute: false }) @@ -21,36 +22,57 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme this._items = this.value ? this.value.map((x) => x.mediaKey) : []; } //TODO: Add support for document specific crops. The server side already supports this. - public get value() { return this.#value; } + @state() + private _startNode: string = ''; + + @state() + private _focalPointEnabled: boolean = false; + + @state() + private _crops: Array = []; + + @state() + private _allowedMediaTypes: Array = []; + public set config(config: UmbPropertyEditorConfigCollection | undefined) { - const validationLimit = config?.getByAlias('validationLimit'); - if (!validationLimit) return; + if (!config) return; - const minMax: Record = validationLimit.value as any; + this._multiple = Boolean(config.getValueByAlias('multiple')); + this._startNode = config.getValueByAlias('startNodeId') ?? ''; + this._focalPointEnabled = Boolean(config.getValueByAlias('enableFocalPoint')); + this._crops = config?.getValueByAlias>('crops') ?? []; - this._limitMin = minMax.min ?? 0; - this._limitMax = minMax.max ?? Infinity; + const filter = config.getValueByAlias('filter'); + this._allowedMediaTypes = filter?.split(',') ?? []; + + const minMax = config.getValueByAlias('validationLimit'); + this._limitMin = minMax?.min ?? 0; + this._limitMax = minMax?.max ?? Infinity; } public get config() { return undefined; } + @state() + private _multiple: boolean = false; + @state() _items: Array = []; @state() private _limitMin: number = 0; + @state() private _limitMax: number = Infinity; #value: Array = []; - #onChange(event: CustomEvent) { - const selection = (event.target as UmbInputMediaElement).selection; + #onChange(event: CustomEvent & { target: UmbInputRichMediaElement }) { + const selection = event.target.selection; const result = selection.map((mediaKey) => { return { @@ -69,12 +91,17 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme render() { return html` - - + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/section-view/media-section-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/section-view/media-section-view.element.ts index 2e3e4b9b26..f496e159af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/section-view/media-section-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/section-view/media-section-view.element.ts @@ -67,7 +67,6 @@ export class UmbMediaSectionViewElement extends UmbLitElement { 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/media/media/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/manifests.ts index 7770970b54..a2ed7ff1dc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/manifests.ts @@ -28,9 +28,10 @@ const treeStore: ManifestTreeStore = { const tree: ManifestTree = { type: 'tree', - kind: 'default', alias: UMB_MEDIA_TREE_ALIAS, name: 'Media Tree', + element: () => import('./media-tree.element.js'), + api: () => import('./media-tree.context.js'), meta: { repositoryAlias: UMB_MEDIA_TREE_REPOSITORY_ALIAS, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.context.ts new file mode 100644 index 0000000000..5861661206 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.context.ts @@ -0,0 +1,22 @@ +import type { UmbMediaTreeItemModel, UmbMediaTreeRootItemsRequestArgs, UmbMediaTreeRootModel } from './types.js'; +import { UMB_CONTENT_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { UmbDefaultTreeContext } from '@umbraco-cms/backoffice/tree'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMediaTreeContext extends UmbDefaultTreeContext< + UmbMediaTreeItemModel, + UmbMediaTreeRootModel, + UmbMediaTreeRootItemsRequestArgs +> { + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_CONTENT_PROPERTY_CONTEXT, (context) => { + this.observe(context.dataType, (value) => { + this.updateAdditionalRequestArgs({ dataType: value }); + }); + }); + } +} + +export { UmbMediaTreeContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.element.ts new file mode 100644 index 0000000000..0b6f3cda83 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.element.ts @@ -0,0 +1,14 @@ +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbDefaultTreeElement } from '@umbraco-cms/backoffice/tree'; + +const elementName = 'umb-media-tree'; +@customElement(elementName) +export class UmbMediaTreeElement extends UmbDefaultTreeElement {} + +export { UmbMediaTreeElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbMediaTreeElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.repository.ts index 8b3478a9ed..d9dbf394ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.repository.ts @@ -1,13 +1,23 @@ import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../entity.js'; import { UmbMediaTreeServerDataSource } from './media-tree.server.data-source.js'; -import type { UmbMediaTreeItemModel, UmbMediaTreeRootModel } from './types.js'; +import type { + UmbMediaTreeChildrenOfRequestArgs, + UmbMediaTreeItemModel, + UmbMediaTreeRootItemsRequestArgs, + UmbMediaTreeRootModel, +} from './types.js'; import { UMB_MEDIA_TREE_STORE_CONTEXT } from './media-tree.store.js'; import { UmbTreeRepositoryBase } from '@umbraco-cms/backoffice/tree'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; export class UmbMediaTreeRepository - extends UmbTreeRepositoryBase + extends UmbTreeRepositoryBase< + UmbMediaTreeItemModel, + UmbMediaTreeRootModel, + UmbMediaTreeRootItemsRequestArgs, + UmbMediaTreeChildrenOfRequestArgs + > implements UmbApi { constructor(host: UmbControllerHost) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts index b8828c58b0..736100cb75 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts @@ -1,5 +1,9 @@ import { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE } from '../entity.js'; -import type { UmbMediaTreeItemModel } from './types.js'; +import type { + UmbMediaTreeChildrenOfRequestArgs, + UmbMediaTreeItemModel, + UmbMediaTreeRootItemsRequestArgs, +} from './types.js'; import type { UmbTreeAncestorsOfRequestArgs, UmbTreeChildrenOfRequestArgs, @@ -17,7 +21,9 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; */ export class UmbMediaTreeServerDataSource extends UmbTreeServerDataSourceBase< MediaTreeItemResponseModel, - UmbMediaTreeItemModel + UmbMediaTreeItemModel, + UmbMediaTreeRootItemsRequestArgs, + UmbMediaTreeChildrenOfRequestArgs > { /** * Creates an instance of UmbMediaTreeServerDataSource. @@ -34,17 +40,18 @@ export class UmbMediaTreeServerDataSource extends UmbTreeServerDataSourceBase< } } -const getRootItems = (args: UmbTreeRootItemsRequestArgs) => +const getRootItems = (args: UmbMediaTreeRootItemsRequestArgs) => // eslint-disable-next-line local-rules/no-direct-api-import - MediaService.getTreeMediaRoot({ skip: args.skip, take: args.take }); + MediaService.getTreeMediaRoot({ dataTypeId: args.dataType?.unique, skip: args.skip, take: args.take }); -const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { +const getChildrenOf = (args: UmbMediaTreeChildrenOfRequestArgs) => { if (args.parent.unique === null) { return getRootItems(args); } else { // eslint-disable-next-line local-rules/no-direct-api-import return MediaService.getTreeMediaChildren({ parentId: args.parent.unique, + dataTypeId: args.dataType?.unique, skip: args.skip, take: args.take, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/types.ts index ff270f525c..33c1979ecd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/types.ts @@ -1,6 +1,11 @@ import type { UmbMediaEntityType, UmbMediaRootEntityType } from '../entity.js'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; -import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import type { + UmbTreeChildrenOfRequestArgs, + UmbTreeItemModel, + UmbTreeRootItemsRequestArgs, + UmbTreeRootModel, +} from '@umbraco-cms/backoffice/tree'; export interface UmbMediaTreeItemModel extends UmbTreeItemModel { entityType: UmbMediaEntityType; @@ -22,3 +27,15 @@ export interface UmbMediaTreeItemVariantModel { name: string; culture: string | null; } + +export interface UmbMediaTreeRootItemsRequestArgs extends UmbTreeRootItemsRequestArgs { + dataType?: { + unique: string; + }; +} + +export interface UmbMediaTreeChildrenOfRequestArgs extends UmbTreeChildrenOfRequestArgs { + dataType?: { + unique: string; + }; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index ca2ed0ef76..c0575bbbde 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -21,8 +21,10 @@ import { import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; 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'; import type { UmbMediaTypeDetailModel } from '@umbraco-cms/backoffice/media-type'; import type { UmbContentWorkspaceContext } from '@umbraco-cms/backoffice/content'; import { UmbEntityContext } from '@umbraco-cms/backoffice/entity'; @@ -408,7 +410,7 @@ export class UmbMediaWorkspaceContext // 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/media/paths.ts b/src/Umbraco.Web.UI.Client/src/packages/media/paths.ts new file mode 100644 index 0000000000..7b524211a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/paths.ts @@ -0,0 +1,6 @@ +import { UMB_SECTION_PATH_PATTERN } from '@umbraco-cms/backoffice/section'; + +export const UMB_MEDIA_SECTION_PATHNAME = 'media'; +export const UMB_MEDIA_SECTION_PATH = UMB_SECTION_PATH_PATTERN.generateAbsolute({ + sectionName: UMB_MEDIA_SECTION_PATHNAME, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts index 054dd486d4..520107daf8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts @@ -15,9 +15,11 @@ import { type UmbContentTypeWorkspaceContext, } from '@umbraco-cms/backoffice/content-type'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; -import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action'; +import { + UmbRequestReloadChildrenOfEntityEvent, + UmbRequestReloadStructureForEntityEvent, +} from '@umbraco-cms/backoffice/entity-action'; type EntityType = UmbMemberTypeDetailModel; export class UmbMemberTypeWorkspaceContext @@ -190,7 +192,7 @@ export class UmbMemberTypeWorkspaceContext await this.structure.create(parent.unique); 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/members/member/collection/action/create-member-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts index 6aaed94120..e7d43ded0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts @@ -15,7 +15,7 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { //TODO: Should we use the tree repository or make a collection repository? //TODO: And how would we get all the member types? //TODO: This only works because member types can't have folders. - const { data } = await this.#memberTypeTreeRepository.requestRootTreeItems({}); + const { data } = await this.#memberTypeTreeRepository.requestTreeRootItems({}); if (!data) return; this._options = data.items.map((item) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts index 071681fa9c..ba3fdaec51 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts @@ -1,9 +1,9 @@ import { UmbMemberTypeTreeRepository } from '../../member-type/tree/member-type-tree.repository.js'; import type { UmbMemberTypeItemModel } from '../../member-type/repository/item/types.js'; import type { UmbMemberCollectionContext } from './member-collection.context.js'; +import { UMB_MEMBER_COLLECTION_CONTEXT } from './member-collection.context-token.js'; import { css, customElement, html, ifDefined, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; @customElement('umb-member-collection-header') export class UmbMemberCollectionHeaderElement extends UmbLitElement { @@ -23,15 +23,15 @@ export class UmbMemberCollectionHeaderElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance as UmbMemberCollectionContext; + this.consumeContext(UMB_MEMBER_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; }); this.#requestContentTypes(); } async #requestContentTypes() { - const { data } = await this.#contentTypeRepository.requestRootTreeItems({}); + const { data } = await this.#contentTypeRepository.requestTreeRootItems({}); if (data) { this._contentTypes = data.items.map((item) => ({ diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context-token.ts new file mode 100644 index 0000000000..f5ab4b661e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context-token.ts @@ -0,0 +1,4 @@ +import type { UmbMemberCollectionContext } from './member-collection.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_MEMBER_COLLECTION_CONTEXT = new UmbContextToken('UmbCollectionContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts index 20d8614c73..fc76b960d7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts @@ -22,4 +22,4 @@ export class UmbMemberCollectionContext extends UmbDefaultCollectionContext< } } -export default UmbMemberCollectionContext; +export { UmbMemberCollectionContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts index f59782c4fc..ce1f3f0d10 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts @@ -1,7 +1,7 @@ import type { UmbMemberCollectionModel } from '../../types.js'; +import { UMB_MEMBER_COLLECTION_CONTEXT } from '../../member-collection.context-token.js'; +import type { UmbMemberCollectionContext } from '../../member-collection.context.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -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 { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -24,12 +24,12 @@ export class UmbMemberTableCollectionViewElement extends UmbLitElement { @state() private _tableItems: Array = []; - #collectionContext?: UmbDefaultCollectionContext; + #collectionContext?: UmbMemberCollectionContext; constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.consumeContext(UMB_MEMBER_COLLECTION_CONTEXT, (instance) => { this.#collectionContext = instance; this.#observeCollectionItems(); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts index 46440b9eea..3d13d9e9c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts @@ -31,7 +31,6 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement( diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/multi-url-picker/multi-url-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/multi-url-picker/multi-url-picker.element.ts index 5600821f11..2a7b140abd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/multi-url-picker/multi-url-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/multi-url-picker/multi-url-picker.element.ts @@ -98,9 +98,6 @@ export class UmbMultiUrlPickerElement extends UUIFormControlMixin(UmbLitElement, @property({ type: Boolean, attribute: 'hide-anchor' }) hideAnchor?: boolean; - @property({ type: Boolean, attribute: 'ignore-user-start-nodes' }) - ignoreUserStartNodes?: boolean; - /** * @type {UUIModalSidebarSize} * @attr @@ -172,7 +169,6 @@ export class UmbMultiUrlPickerElement extends UUIFormControlMixin(UmbLitElement, index: index, config: { hideAnchor: this.hideAnchor, - ignoreUserStartNodes: this.ignoreUserStartNodes, }, }, value: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/property-editor/property-editor-ui-multi-url-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/property-editor/property-editor-ui-multi-url-picker.element.ts index ee10b18853..354f0ff07d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/property-editor/property-editor-ui-multi-url-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/property-editor/property-editor-ui-multi-url-picker.element.ts @@ -23,7 +23,6 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement impl if (!config) return; this._hideAnchor = config.getValueByAlias('hideAnchor') ?? false; - this._ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes') ?? false; this._minNumber = Number(config.getValueByAlias('minNumber')) ?? 0; this._maxNumber = Number(config.getValueByAlias('maxNumber')) ?? Infinity; this._overlaySize = config.getValueByAlias('overlaySize') ?? 'small'; @@ -35,9 +34,6 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement impl @state() private _hideAnchor?: boolean; - @state() - private _ignoreUserStartNodes?: boolean; - @state() private _minNumber? = 0; @@ -68,7 +64,6 @@ export class UmbPropertyEditorUIMultiUrlPickerElement extends UmbLitElement impl return html` ('ignoreUserStartNodes') ?? false, - }, + config: {}, index: null, }, value: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/packages/package-builder/workspace/workspace-package-builder.element.ts b/src/Umbraco.Web.UI.Client/src/packages/packages/package-builder/workspace/workspace-package-builder.element.ts index b388e2e0ae..e4809f0ad0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/packages/package-builder/workspace/workspace-package-builder.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/packages/package-builder/workspace/workspace-package-builder.element.ts @@ -229,7 +229,10 @@ export class UmbWorkspacePackageBuilderElement extends UmbLitElement { return html`
- + `; } @@ -130,7 +122,6 @@ export class UmbInputContentElement extends UUIFormControlMixin(UmbLitElement, ' .min=${this.min} .max=${this.max} ?showOpenButton=${this.showOpenButton} - ?ignoreUserStartNodes=${this.ignoreUserStartNodes} @change=${this.#onChange}>`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts index 780f1ea40c..18d21e1fd5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts @@ -38,9 +38,6 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple @state() _showOpenButton?: boolean; - @state() - _ignoreUserStartNodes?: boolean; - @state() _rootUnique?: string | null; @@ -72,7 +69,6 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple this._allowedContentTypeUniques = config.getValueByAlias('filter'); this._showOpenButton = config.getValueByAlias('showOpenButton'); - this._ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes'); } connectedCallback() { @@ -118,7 +114,6 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple .startNode=${startNode} .allowedContentTypeIds=${this._allowedContentTypeUniques ?? ''} ?showOpenButton=${this._showOpenButton} - ?ignoreUserStartNodes=${this._ignoreUserStartNodes} @change=${this.#onChange}>`; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/umb-search-header-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/search/umb-search-header-app.element.ts index f1d7b604ad..4e17d9896a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/umb-search-header-app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/search/umb-search-header-app.element.ts @@ -1,25 +1,13 @@ import { UMB_SEARCH_MODAL } from './search-modal/search-modal.token.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; -import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; +import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbHeaderAppButtonElement } from '@umbraco-cms/backoffice/components'; @customElement('umb-search-header-app') -export class UmbSearchHeaderAppElement extends UmbLitElement { - private _modalContext?: UmbModalManagerContext; - - constructor() { - super(); - - this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (_instance) => { - this._modalContext = _instance; - }); - } - - #onSearchClick() { - this._modalContext?.open(this, UMB_SEARCH_MODAL); +export class UmbSearchHeaderAppElement extends UmbHeaderAppButtonElement { + async #onSearchClick() { + const context = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + context.open(this, UMB_SEARCH_MODAL); } render() { @@ -30,15 +18,7 @@ export class UmbSearchHeaderAppElement extends UmbLitElement { `; } - static styles: CSSResultGroup = [ - UmbTextStyles, - css` - uui-button { - font-size: 18px; - --uui-button-background-color: transparent; - } - `, - ]; + static styles = UmbHeaderAppButtonElement.styles; } export default UmbSearchHeaderAppElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts index 6ee8bbca69..2a520a2f87 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts @@ -14,8 +14,10 @@ import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import { PartialViewService } from '@umbraco-cms/backoffice/external/backend-api'; 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'; import type { IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; export class UmbPartialViewWorkspaceContext @@ -174,7 +176,7 @@ export class UmbPartialViewWorkspaceContext // 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/templating/scripts/workspace/script-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts index d13fd8900b..fb5eb59679 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts @@ -14,8 +14,10 @@ import { } from '@umbraco-cms/backoffice/workspace'; import { loadCodeEditor } from '@umbraco-cms/backoffice/code-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'; import type { IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; export class UmbScriptWorkspaceContext @@ -154,7 +156,7 @@ export class UmbScriptWorkspaceContext // 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/templating/stylesheets/workspace/stylesheet-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts index 01d9df95c2..83324d5dba 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts @@ -14,8 +14,10 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { loadCodeEditor } from '@umbraco-cms/backoffice/code-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'; import type { IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; export class UmbStylesheetWorkspaceContext @@ -153,7 +155,7 @@ export class UmbStylesheetWorkspaceContext // 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/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts index 40613982b0..1ca7979eba 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts @@ -13,8 +13,10 @@ import { import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; 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'; import type { IRoutingInfo, PageComponent } from '@umbraco-cms/backoffice/router'; export class UmbTemplateWorkspaceContext @@ -201,7 +203,7 @@ ${currentContent}`; // 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/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index 672bd4cfff..14fbbb6d5f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -6,12 +6,7 @@ import type { UMB_CURRENT_USER_CONTEXT, UmbCurrentUserModel } from '@umbraco-cms import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce'; import { UmbTemporaryFileRepository } from '@umbraco-cms/backoffice/temporary-file'; import { UmbId } from '@umbraco-cms/backoffice/id'; -import { - sizeImageInEditor, - uploadBlobImages, - UMB_MEDIA_TREE_PICKER_MODAL, - UMB_MEDIA_PICKER_MODAL, -} from '@umbraco-cms/backoffice/media'; +import { sizeImageInEditor, uploadBlobImages, UMB_MEDIA_PICKER_MODAL } from '@umbraco-cms/backoffice/media'; interface MediaPickerTargetData { altText?: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-header-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-header-app.element.ts index d0e5aaa917..5e8f763901 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-header-app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/current-user-header-app.element.ts @@ -1,13 +1,12 @@ import { UMB_CURRENT_USER_MODAL } from './modals/current-user/current-user-modal.token.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_CURRENT_USER_CONTEXT, type UmbCurrentUserModel } from '@umbraco-cms/backoffice/current-user'; +import { UmbHeaderAppButtonElement } from '@umbraco-cms/backoffice/components'; @customElement('umb-current-user-header-app') -export class UmbCurrentUserHeaderAppElement extends UmbLitElement { +export class UmbCurrentUserHeaderAppElement extends UmbHeaderAppButtonElement { @state() private _currentUser?: UmbCurrentUserModel; @@ -96,11 +95,10 @@ export class UmbCurrentUserHeaderAppElement extends UmbLitElement { } static styles: CSSResultGroup = [ - UmbTextStyles, + UmbHeaderAppButtonElement.styles, css` uui-button { font-size: 14px; - --uui-button-background-color: transparent; } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/configure-external-login-providers-action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/configure-external-login-providers-action.ts new file mode 100644 index 0000000000..a3706c2988 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/configure-external-login-providers-action.ts @@ -0,0 +1,18 @@ +import { UMB_CURRENT_USER_EXTERNAL_LOGIN_MODAL } from './modals/external-login-modal.token.js'; +import { UmbActionBase } from '@umbraco-cms/backoffice/action'; +import type { UmbCurrentUserAction, UmbCurrentUserActionArgs } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; + +export class UmbConfigureExternalLoginProvidersApi + extends UmbActionBase> + implements UmbCurrentUserAction +{ + async getHref() { + return undefined; + } + + async execute() { + const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + await modalManagerContext.open(this, UMB_CURRENT_USER_EXTERNAL_LOGIN_MODAL).onSubmit(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts new file mode 100644 index 0000000000..bef139a91b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/manifests.ts @@ -0,0 +1,33 @@ +import { UmbConfigureExternalLoginProvidersApi } from './configure-external-login-providers-action.js'; +import type { ManifestCurrentUserActionDefaultKind, ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; + +export const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.CurrentUserExternalLogin', + name: 'External Login Modal', + js: () => import('./modals/external-login-modal.element.js'), + }, +]; + +export const userProfileApps: Array = [ + { + type: 'currentUserAction', + kind: 'default', + alias: 'Umb.CurrentUser.App.ExternalLoginProviders', + name: 'External Login Providers Current User App', + weight: 700, + api: UmbConfigureExternalLoginProvidersApi, + meta: { + label: '#defaultdialogs_externalLoginProviders', + icon: 'icon-lock', + look: 'secondary', + }, + conditions: [ + { + alias: 'Umb.Condition.User.AllowExternalLoginAction', + }, + ], + }, +]; +export const manifests = [...modals, ...userProfileApps]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts new file mode 100644 index 0000000000..7804f8ab92 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.element.ts @@ -0,0 +1,241 @@ +import { UmbCurrentUserRepository } from '../../repository/index.js'; +import type { UmbCurrentUserExternalLoginProviderModel } from '../../types.js'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbConfirmModal, type UmbModalContext } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api'; + +type UmbExternalLoginProviderOption = UmbCurrentUserExternalLoginProviderModel & { + displayName: string; + icon?: string; + existsOnServer: boolean; +}; + +@customElement('umb-current-user-external-login-modal') +export class UmbCurrentUserExternalLoginModalElement extends UmbLitElement { + @property({ attribute: false }) + modalContext?: UmbModalContext; + + @state() + _items: Array = []; + + #currentUserRepository = new UmbCurrentUserRepository(this); + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + + constructor() { + super(); + this.#loadProviders(); + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => { + this.#notificationContext = context; + }); + } + + async #loadProviders() { + const serverLoginProviders$ = (await this.#currentUserRepository.requestExternalLoginProviders()).asObservable(); + const manifestLoginProviders$ = umbExtensionsRegistry.byTypeAndFilter( + 'authProvider', + (ext) => !!ext.meta?.linking?.allowManualLinking, + ); + + // Merge the server and manifest providers to get the final list of providers + const externalLoginProviders$ = mergeObservables( + [serverLoginProviders$, manifestLoginProviders$], + ([serverLoginProviders, manifestLoginProviders]) => { + const providers: UmbExternalLoginProviderOption[] = manifestLoginProviders.map((manifestLoginProvider) => { + const serverLoginProvider = serverLoginProviders.find( + (serverLoginProvider) => serverLoginProvider.providerSchemeName === manifestLoginProvider.forProviderName, + ); + return { + existsOnServer: !!serverLoginProvider, + hasManualLinkingEnabled: serverLoginProvider?.hasManualLinkingEnabled ?? false, + isLinkedOnUser: serverLoginProvider?.isLinkedOnUser ?? false, + providerKey: serverLoginProvider?.providerKey ?? '', + providerSchemeName: manifestLoginProvider.forProviderName, + icon: manifestLoginProvider.meta?.defaultView?.icon, + displayName: + manifestLoginProvider.meta?.label ?? manifestLoginProvider.forProviderName ?? manifestLoginProvider.name, + } satisfies UmbExternalLoginProviderOption; + }); + + return providers; + }, + ); + + this.observe( + externalLoginProviders$, + (providers) => { + this._items = providers; + }, + '_externalLoginProviders', + ); + } + + #close() { + this.modalContext?.submit(); + } + + render() { + return html` + +
+ ${repeat( + this._items, + (item) => item.providerSchemeName, + (item) => this.#renderProvider(item), + )} +
+
+ +
+
+ `; + } + + /** + * Render a provider with a toggle to enable/disable it + */ + #renderProvider(item: UmbExternalLoginProviderOption) { + return html` + +
+ + ${this.localize.string(item.displayName)} +
+ ${when( + item.existsOnServer, + () => nothing, + () => + html`
+ + ! + +
`, + )} + ${when( + item.isLinkedOnUser, + () => html` +

+ Your account is linked to this service +

+ this.#onProviderDisable(item)}> + + Unlink your ${this.localize.string(item.displayName)} account + + + + `, + () => html` + this.#onProviderEnable(item)}> + + Link your ${this.localize.string(item.displayName)} account + + + + `, + )} +
+ `; + } + + async #onProviderEnable(item: UmbExternalLoginProviderOption) { + const providerDisplayName = this.localize.string(item.displayName); + try { + await umbConfirmModal(this, { + headline: this.localize.term('defaultdialogs_linkYour', providerDisplayName), + content: this.localize.term('defaultdialogs_linkYourConfirm', providerDisplayName), + confirmLabel: this.localize.term('general_continue'), + color: 'positive', + }); + const authContext = await this.getContext(UMB_AUTH_CONTEXT); + await authContext.linkLogin(item.providerSchemeName); + } catch (error) { + if (error instanceof Error) { + this.#notificationContext?.peek('danger', { + data: { + headline: this.localize.term('defaultdialogs_linkYour', providerDisplayName), + message: error.message, + }, + }); + } + } + } + + async #onProviderDisable(item: UmbExternalLoginProviderOption) { + if (!item.providerKey) { + throw new Error('Provider key is missing'); + } + + const providerDisplayName = this.localize.string(item.displayName); + try { + await umbConfirmModal(this, { + headline: this.localize.term('defaultdialogs_unLinkYour', providerDisplayName), + content: this.localize.term('defaultdialogs_unLinkYourConfirm', providerDisplayName), + confirmLabel: this.localize.term('general_continue'), + color: 'danger', + }); + const authContext = await this.getContext(UMB_AUTH_CONTEXT); + await authContext.unlinkLogin(item.providerSchemeName, item.providerKey); + } catch (error) { + let message = this.localize.term('errors_receivedErrorFromServer'); + if (error instanceof Error) { + message = error.message; + } else if (typeof error === 'object' && (error as ProblemDetails).title) { + message = (error as ProblemDetails).title ?? message; + } + console.error('[External Login] Error unlinking provider: ', error); + this.#notificationContext?.peek('danger', { + data: { + headline: this.localize.term('defaultdialogs_unLinkYour', providerDisplayName), + message, + }, + }); + } + } + + static styles = [ + UmbTextStyles, + css` + uui-box { + margin-bottom: var(--uui-size-space-3); + } + + .header { + display: flex; + align-items: center; + } + + .header-icon { + margin-right: var(--uui-size-space-4); + } + `, + ]; +} + +export default UmbCurrentUserExternalLoginModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-current-user-external-login-modal': UmbCurrentUserExternalLoginModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts new file mode 100644 index 0000000000..0cb3740eee --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.stories.ts @@ -0,0 +1,46 @@ +import type { Meta, StoryObj } from '@storybook/web-components'; +import type { UmbCurrentUserExternalLoginModalElement } from './external-login-modal.element.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +import './external-login-modal.element.js'; + +class UmbServerExtensionsHostElement extends UmbLitElement { + constructor() { + super(); + new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPublicExtensions(); + } + + render() { + return html``; + } +} + +if (window.customElements.get('umb-server-extensions-host') === undefined) { + customElements.define('umb-server-extensions-host', UmbServerExtensionsHostElement); +} + +const meta: Meta = { + title: 'Current User/External Login/Configure External Login Providers', + component: 'umb-current-user-external-login-modal', + decorators: [ + (Story) => + html` + ${Story()} + `, + ], + parameters: { + layout: 'centered', + actions: { + disabled: true, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Overview: Story = {}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.token.ts new file mode 100644 index 0000000000..52f0adae32 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/external-login/modals/external-login-modal.token.ts @@ -0,0 +1,8 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export const UMB_CURRENT_USER_EXTERNAL_LOGIN_MODAL = new UmbModalToken('Umb.Modal.CurrentUserExternalLogin', { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts index e3212adbe1..dce911901e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/manifests.ts @@ -1,6 +1,7 @@ import { manifest as actionDefaultKindManifest } from './action/default.kind.js'; import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as historyManifests } from './history/manifests.js'; +import { manifests as externalLoginProviderManifests } from './external-login/manifests.js'; import { manifests as mfaLoginProviderManifests } from './mfa-login/manifests.js'; import { manifests as profileManifests } from './profile/manifests.js'; import { manifests as themeManifests } from './theme/manifests.js'; @@ -32,6 +33,7 @@ export const manifests = [ actionDefaultKindManifest, ...headerApps, ...historyManifests, + ...externalLoginProviderManifests, ...mfaLoginProviderManifests, ...modalManifests, ...profileManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts index ffa32541ae..a740321f32 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/modals/current-user-mfa/current-user-mfa-modal.element.ts @@ -2,7 +2,7 @@ import { UMB_CURRENT_USER_MFA_ENABLE_PROVIDER_MODAL } from '../current-user-mfa- import { UmbCurrentUserRepository } from '../../repository/index.js'; import { UMB_CURRENT_USER_MFA_DISABLE_PROVIDER_MODAL } from '../current-user-mfa-disable-provider/current-user-mfa-disable-provider-modal.token.js'; import type { UmbCurrentUserMfaProviderModel } from '../../types.js'; -import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_MODAL_MANAGER_CONTEXT, type UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -11,6 +11,7 @@ import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; type UmbMfaLoginProviderOption = UmbCurrentUserMfaProviderModel & { displayName: string; + existsOnServer: boolean; }; @customElement('umb-current-user-mfa-modal') @@ -41,6 +42,7 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { (serverLoginProvider) => serverLoginProvider.providerName === manifestLoginProvider.forProviderName, ); return { + existsOnServer: !!serverLoginProvider, isEnabledOnUser: serverLoginProvider?.isEnabledOnUser ?? false, providerName: serverLoginProvider?.providerName ?? manifestLoginProvider.forProviderName, displayName: @@ -91,6 +93,20 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { #renderProvider(item: UmbMfaLoginProviderOption) { return html` + ${when( + item.existsOnServer, + () => nothing, + () => + html`
+ + ! + +
`, + )} ${when( item.isEnabledOnUser, () => html` @@ -108,6 +124,7 @@ export class UmbCurrentUserMfaModalElement extends UmbLitElement { this.#onProviderEnable(item)}> `, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts index 40026ffe1a..ca0db09ecb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.repository.ts @@ -40,6 +40,21 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase { return { data, error, asObservable: () => this.#currentUserStore!.data }; } + /** + * Request the current user's external login providers + * @memberof UmbCurrentUserRepository + */ + async requestExternalLoginProviders() { + await this.#init; + const { data, error } = await this.#currentUserSource.getExternalLoginProviders(); + + if (data) { + this.#currentUserStore?.setExternalLoginProviders(data); + } + + return { data, error, asObservable: () => this.#currentUserStore!.externalLoginProviders }; + } + /** * Request the current user's available MFA login providers * @memberof UmbCurrentUserRepository diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts index 389d856f9a..84e2546336 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts @@ -40,6 +40,7 @@ export class UmbCurrentUserServerDataSource { email: data.email, fallbackPermissions: data.fallbackPermissions, hasAccessToAllLanguages: data.hasAccessToAllLanguages, + hasAccessToSensitiveData: data.hasAccessToSensitiveData, hasDocumentRootAccess: data.hasDocumentRootAccess, hasMediaRootAccess: data.hasMediaRootAccess, isAdmin: data.isAdmin, @@ -61,6 +62,14 @@ export class UmbCurrentUserServerDataSource { return { error }; } + /** + * Get the current user's external login providers + * @memberof UmbCurrentUserServerDataSource + */ + async getExternalLoginProviders() { + return tryExecuteAndNotify(this.#host, UserService.getUserCurrentLoginProviders()); + } + /** * Get the current user's available MFA login providers * @memberof UmbCurrentUserServerDataSource diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts index 1e6a9ffa1f..684e41e7dd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.store.ts @@ -1,4 +1,8 @@ -import type { UmbCurrentUserMfaProviderModel, UmbCurrentUserModel } from '../types.js'; +import type { + UmbCurrentUserExternalLoginProviderModel, + UmbCurrentUserMfaProviderModel, + UmbCurrentUserModel, +} from '../types.js'; import type { UmbUserDetailModel } from '@umbraco-cms/backoffice/user'; import { UMB_USER_DETAIL_STORE_CONTEXT } from '@umbraco-cms/backoffice/user'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; @@ -13,6 +17,12 @@ export class UmbCurrentUserStore extends UmbContextBase { #mfaProviders = new UmbArrayState([], (e) => e.providerName); readonly mfaProviders = this.#mfaProviders.asObservable(); + #externalLoginProviders = new UmbArrayState( + [], + (e) => e.providerSchemeName, + ); + readonly externalLoginProviders = this.#externalLoginProviders.asObservable(); + constructor(host: UmbControllerHost) { super(host, UMB_CURRENT_USER_STORE_CONTEXT); @@ -85,6 +95,14 @@ export class UmbCurrentUserStore extends UmbContextBase { updateMfaProvider(data: Partial) { this.#mfaProviders.updateOne(data.providerName, data); } + + setExternalLoginProviders(data: Array) { + this.#externalLoginProviders.setValue(data); + } + + updateExternalLoginProvider(data: Partial) { + this.#externalLoginProviders.updateOne(data.providerSchemeName, data); + } } export const UMB_CURRENT_USER_STORE_CONTEXT = new UmbContextToken('UmbCurrentUserStore'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts index 6a1363cc9a..0453be866b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts @@ -3,6 +3,7 @@ import type { CancelError, DocumentPermissionPresentationModel, UnknownTypePermissionPresentationModel, + UserExternalLoginProviderModel, UserTwoFactorProviderModel, } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; @@ -14,6 +15,7 @@ export interface UmbCurrentUserModel { email: string; fallbackPermissions: Array; hasAccessToAllLanguages: boolean; + hasAccessToSensitiveData: boolean; hasDocumentRootAccess: boolean; hasMediaRootAccess: boolean; isAdmin: boolean; @@ -26,6 +28,8 @@ export interface UmbCurrentUserModel { userName: string; } +export type UmbCurrentUserExternalLoginProviderModel = UserExternalLoginProviderModel; + export type UmbCurrentUserMfaProviderModel = UserTwoFactorProviderModel; export type UmbMfaProviderConfigurationCallback = Promise<{ error?: ApiError | CancelError }>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection-header.element.ts index cb36a39677..d29aa708ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection-header.element.ts @@ -1,7 +1,7 @@ import type { UmbUserGroupCollectionContext } from './user-group-collection.context.js'; +import { UMB_USER_GROUP_COLLECTION_CONTEXT } from './user-group-collection.context-token.js'; import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; import { debounce } from '@umbraco-cms/backoffice/utils'; const elementName = 'umb-user-group-collection-header'; @@ -12,8 +12,8 @@ export class UmbUserGroupCollectionHeaderElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance as UmbUserGroupCollectionContext; + this.consumeContext(UMB_USER_GROUP_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection.context-token.ts new file mode 100644 index 0000000000..641d8e541e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/user-group-collection.context-token.ts @@ -0,0 +1,6 @@ +import type { UmbUserGroupCollectionContext } from './user-group-collection.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_USER_GROUP_COLLECTION_CONTEXT = new UmbContextToken( + 'UmbCollectionContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts index 25dbeb6eb7..168dfb8509 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts @@ -1,9 +1,9 @@ +import { UMB_USER_GROUP_COLLECTION_CONTEXT } from '../user-group-collection.context-token.js'; import type { UmbUserGroupDetailModel } from '../../types.js'; +import type { UmbUserGroupCollectionContext } from '../user-group-collection.context.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; import type { UmbTableColumn, UmbTableConfig, @@ -55,7 +55,7 @@ export class UmbUserGroupCollectionTableViewElement extends UmbLitElement { @state() private _selection: Array = []; - #collectionContext?: UmbDefaultCollectionContext; + #collectionContext?: UmbUserGroupCollectionContext; // TODO: hardcoded dependencies on document and media modules. We should figure out how these dependencies can be added through extensions. #documentItemRepository = new UmbDocumentItemRepository(this); @@ -67,7 +67,7 @@ export class UmbUserGroupCollectionTableViewElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { + this.consumeContext(UMB_USER_GROUP_COLLECTION_CONTEXT, (instance) => { this.#collectionContext = instance; this.observe( this.#collectionContext.selection.selection, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/action/create-user.collection-action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/action/create-user.collection-action.ts index 2b7f078d9f..73ac577b54 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/action/create-user.collection-action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/action/create-user.collection-action.ts @@ -1,11 +1,32 @@ import { UMB_CREATE_USER_MODAL } from '../../modals/create/create-user-modal.token.js'; -import { UmbCollectionActionBase } from '@umbraco-cms/backoffice/collection'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; +import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -export class UmbCreateUserCollectionAction extends UmbCollectionActionBase { +export class UmbCreateUserCollectionAction extends UmbControllerBase { async execute() { const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + + const unique = entityContext.getUnique(); + const entityType = entityContext.getEntityType(); + + if (unique === undefined) throw new Error('Missing unique'); + if (!entityType) throw new Error('Missing entityType'); + const modalContext = modalManager.open(this, UMB_CREATE_USER_MODAL); - await modalContext?.onSubmit(); + modalContext?.onSubmit().catch(async () => { + // modal is closed after creation instead of navigating to the new user. + // We therefore need to reload the children of the entity + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadChildrenOfEntityEvent({ + entityType, + unique, + }); + + eventContext.dispatchEvent(event); + }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/types.ts index a1d5e5c99a..3417ee3dd7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/types.ts @@ -1,14 +1,12 @@ import type { UmbUserOrderByType, UmbUserStateFilterType } from './utils/index.js'; +import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; import type { UmbDirectionType } from '@umbraco-cms/backoffice/utils'; -export interface UmbUserCollectionFilterModel { - skip?: number; - take?: number; +export interface UmbUserCollectionFilterModel extends UmbCollectionFilterModel { orderBy?: UmbUserOrderByType; orderDirection?: UmbDirectionType; userGroupIds?: string[]; userStates?: UmbUserStateFilterType[]; - filter?: string; } export interface UmbUserOrderByOption { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection-header.element.ts index c55d9b6352..54c55a01b6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection-header.element.ts @@ -2,10 +2,10 @@ import type { UmbUserCollectionContext } from './user-collection.context.js'; import type { UmbUserOrderByOption } from './types.js'; import type { UmbUserStateFilterType } from './utils/index.js'; import { UmbUserStateFilter } from './utils/index.js'; +import { UMB_USER_COLLECTION_CONTEXT } from './user-collection.context-token.js'; import type { UUIBooleanInputEvent, UUICheckboxElement } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; import type { UmbUserGroupDetailModel } from '@umbraco-cms/backoffice/user-group'; import { UmbUserGroupCollectionRepository } from '@umbraco-cms/backoffice/user-group'; import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; @@ -39,8 +39,8 @@ export class UmbUserCollectionHeaderElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance as UmbUserCollectionContext; + this.consumeContext(UMB_USER_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; this.#observeOrderByOptions(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context-token.ts new file mode 100644 index 0000000000..05e99d2ebe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context-token.ts @@ -0,0 +1,4 @@ +import type { UmbUserCollectionContext } from './user-collection.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_USER_COLLECTION_CONTEXT = new UmbContextToken('UmbCollectionContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts index 3c81c52ace..1c0e587755 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/user-collection.context.ts @@ -122,4 +122,4 @@ export class UmbUserCollectionContext extends UmbDefaultCollectionContext< } } -export default UmbUserCollectionContext; +export { UmbUserCollectionContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/grid/user-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/grid/user-grid-collection-view.element.ts index eaa00e61ee..5451378b1c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/grid/user-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/grid/user-grid-collection-view.element.ts @@ -1,9 +1,9 @@ import { getDisplayStateFromUserStatus } from '../../../utils.js'; import type { UmbUserCollectionContext } from '../../user-collection.context.js'; import type { UmbUserDetailModel } from '../../../types.js'; +import { UMB_USER_COLLECTION_CONTEXT } from '../../user-collection.context-token.js'; import { css, html, nothing, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UserStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbUserGroupDetailModel } from '@umbraco-cms/backoffice/user-group'; @@ -29,8 +29,8 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance as UmbUserCollectionContext; + this.consumeContext(UMB_USER_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; this.observe( this.#collectionContext.selection.selection, (selection) => (this._selection = selection), diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts index b2eab75172..db0ca43c0e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/views/table/user-table-collection-view.element.ts @@ -1,5 +1,6 @@ import type { UmbUserCollectionContext } from '../../user-collection.context.js'; import type { UmbUserDetailModel } from '../../../types.js'; +import { UMB_USER_COLLECTION_CONTEXT } from '../../user-collection.context-token.js'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { @@ -11,7 +12,6 @@ import type { UmbTableConfig, UmbTableOrderedEvent, } from '@umbraco-cms/backoffice/components'; -import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbUserGroupItemModel } from '@umbraco-cms/backoffice/user-group'; import { UmbUserGroupItemRepository } from '@umbraco-cms/backoffice/user-group'; @@ -68,8 +68,8 @@ export class UmbUserTableCollectionViewElement extends UmbLitElement { constructor() { super(); - this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => { - this.#collectionContext = instance as UmbUserCollectionContext; + this.consumeContext(UMB_USER_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; this.observe( this.#collectionContext.selection.selection, (selection) => (this._selection = selection), diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts index d1348a4254..04150acb7c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/manifests.ts @@ -1,6 +1,7 @@ import { manifest as userAllowDisableActionManifest } from './user-allow-disable-action.condition.js'; import { manifest as userAllowEnableActionManifest } from './user-allow-enable-action.condition.js'; import { manifest as userAllowUnlockActionManifest } from './user-allow-unlock-action.condition.js'; +import { manifest as userAllowExternalLoginActionManifest } from './user-allow-external-login-action.condition.js'; import { manifest as userAllowMfaActionManifest } from './user-allow-mfa-action.condition.js'; import { manifest as userAllowDeleteActionManifest } from './user-allow-delete-action.condition.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; @@ -9,6 +10,7 @@ export const manifests: Array = [ userAllowDisableActionManifest, userAllowEnableActionManifest, userAllowUnlockActionManifest, + userAllowExternalLoginActionManifest, userAllowMfaActionManifest, userAllowDeleteActionManifest, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts new file mode 100644 index 0000000000..05af0c57d0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-external-login-action.condition.ts @@ -0,0 +1,24 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { ManifestCondition } from '@umbraco-cms/backoffice/extension-api'; +import { UmbConditionBase, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +export class UmbUserAllowExternalLoginActionCondition extends UmbConditionBase { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(host: UmbControllerHost, args: any) { + super(host, args); + + // Check if there are any MFA providers available + this.observe( + umbExtensionsRegistry.byType('authProvider'), + (exts) => (this.permitted = exts.length > 0 && exts.some((ext) => ext.meta?.linking?.allowManualLinking)), + '_userAllowExternalLoginActionConditionProviders', + ); + } +} + +export const manifest: ManifestCondition = { + type: 'condition', + name: 'User Allow ExternalLogin Action Condition', + alias: 'Umb.Condition.User.AllowExternalLoginAction', + api: UmbUserAllowExternalLoginActionCondition, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/disable/disable.action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/disable/disable.action.ts index 2e93cba594..d1e873e188 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/disable/disable.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/disable/disable.action.ts @@ -1,9 +1,27 @@ import { UmbDisableUserRepository } from '../../repository/index.js'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; +import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; +import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; export class UmbDisableUserEntityBulkAction extends UmbEntityBulkActionBase { async execute() { const repository = new UmbDisableUserRepository(this._host); await repository.disable(this.selection); + + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (!entityType) throw new Error('Entity type not found'); + if (unique === undefined) throw new Error('Entity unique not found'); + + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadChildrenOfEntityEvent({ + entityType, + unique, + }); + + eventContext.dispatchEvent(event); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/enable/enable.action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/enable/enable.action.ts index b663558dd0..3236083032 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/enable/enable.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/enable/enable.action.ts @@ -1,9 +1,27 @@ import { UmbEnableUserRepository } from '../../repository/index.js'; +import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; export class UmbEnableUserEntityBulkAction extends UmbEntityBulkActionBase { async execute() { const repository = new UmbEnableUserRepository(this._host); await repository.enable(this.selection); + + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (!entityType) throw new Error('Entity type not found'); + if (unique === undefined) throw new Error('Entity unique not found'); + + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadChildrenOfEntityEvent({ + entityType, + unique, + }); + + eventContext.dispatchEvent(event); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/unlock/unlock.action.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/unlock/unlock.action.ts index 3dcc43fd92..b2d00aa668 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/unlock/unlock.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-bulk-actions/unlock/unlock.action.ts @@ -1,9 +1,27 @@ import { UmbUnlockUserRepository } from '../../repository/index.js'; +import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; export class UmbUnlockUserEntityBulkAction extends UmbEntityBulkActionBase { async execute() { const repository = new UmbUnlockUserRepository(this._host); await repository.unlock(this.selection); + + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (!entityType) throw new Error('Entity type not found'); + if (unique === undefined) throw new Error('Entity unique not found'); + + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadChildrenOfEntityEvent({ + entityType, + unique, + }); + + eventContext.dispatchEvent(event); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity.ts index add1de029d..36aafabbcf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity.ts @@ -1,3 +1,5 @@ export const UMB_USER_ENTITY_TYPE = 'user'; +export const UMB_USER_ROOT_ENTITY_TYPE = 'user-root'; export type UmbUserEntityType = typeof UMB_USER_ENTITY_TYPE; +export type UmbUserRootEntityType = typeof UMB_USER_ROOT_ENTITY_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/create/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/create/constants.ts new file mode 100644 index 0000000000..96e8219440 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/create/constants.ts @@ -0,0 +1 @@ +export const UMB_CREATE_USER_MODAL_ALIAS = 'Umb.Modal.User.Create'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/create/create-user-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/create/create-user-modal.token.ts index b017d23706..d8e2edf546 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/create/create-user-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/create/create-user-modal.token.ts @@ -1,6 +1,7 @@ +import { UMB_CREATE_USER_MODAL_ALIAS } from './constants.js'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -export const UMB_CREATE_USER_MODAL = new UmbModalToken('Umb.Modal.User.Create', { +export const UMB_CREATE_USER_MODAL = new UmbModalToken(UMB_CREATE_USER_MODAL_ALIAS, { modal: { type: 'dialog', size: 'small', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts index bfb4ce9b21..0f32cb6f68 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts @@ -1,9 +1,10 @@ +import { UMB_CREATE_USER_MODAL_ALIAS } from './create/constants.js'; import type { ManifestModal, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const modals: Array = [ { type: 'modal', - alias: 'Umb.Modal.User.Create', + alias: UMB_CREATE_USER_MODAL_ALIAS, name: 'Create User Modal', js: () => import('./create/create-user-modal.element.js'), }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts index c10e921421..74805b9d30 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-mfa/user-mfa-modal.element.ts @@ -1,7 +1,7 @@ import { UmbUserRepository } from '../../repository/index.js'; import type { UmbUserMfaProviderModel } from '../../types.js'; import type { UmbUserMfaModalConfiguration } from './user-mfa-modal.token.js'; -import { css, customElement, html, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { umbConfirmModal, type UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @@ -10,6 +10,7 @@ import { mergeObservables } from '@umbraco-cms/backoffice/observable-api'; type UmbMfaLoginProviderOption = UmbUserMfaProviderModel & { displayName: string; + existsOnServer: boolean; }; @customElement('umb-user-mfa-modal') @@ -41,6 +42,7 @@ export class UmbUserMfaModalElement extends UmbLitElement { (serverLoginProvider) => serverLoginProvider.providerName === manifestLoginProvider.forProviderName, ); return { + existsOnServer: !!serverLoginProvider, isEnabledOnUser: serverLoginProvider?.isEnabledOnUser ?? false, providerName: serverLoginProvider?.providerName ?? manifestLoginProvider.forProviderName, displayName: @@ -91,6 +93,20 @@ export class UmbUserMfaModalElement extends UmbLitElement { #renderProvider(item: UmbMfaLoginProviderOption) { return html` + ${when( + item.existsOnServer, + () => nothing, + () => + html`
+ + ! + +
`, + )} ${when( item.isEnabledOnUser, () => html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/section-view/users-section-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/section-view/users-section-view.element.ts index 52eb5e4152..43c5e50886 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/section-view/users-section-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/section-view/users-section-view.element.ts @@ -1,11 +1,12 @@ import { UMB_USER_COLLECTION_ALIAS } from '../collection/manifests.js'; -import { UMB_USER_ENTITY_TYPE } from '../entity.js'; +import { UMB_USER_ENTITY_TYPE, UMB_USER_ROOT_ENTITY_TYPE } from '../entity.js'; import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbRoute } from '@umbraco-cms/backoffice/router'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbCollectionElement } from '@umbraco-cms/backoffice/collection'; import { UmbWorkspaceElement } from '@umbraco-cms/backoffice/workspace'; +import { UmbEntityContext } from '@umbraco-cms/backoffice/entity'; @customElement('umb-section-view-users') export class UmbSectionViewUsersElement extends UmbLitElement { @@ -14,6 +15,9 @@ export class UmbSectionViewUsersElement extends UmbLitElement { path: 'collection', component: () => { const element = new UmbCollectionElement(); + const entityContext = new UmbEntityContext(element); + entityContext.setEntityType(UMB_USER_ROOT_ENTITY_TYPE); + entityContext.setUnique(null); element.setAttribute('alias', UMB_USER_COLLECTION_ALIAS); return element; }, diff --git a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs index 23006d64e2..09b67e621e 100644 --- a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs +++ b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs @@ -50,6 +50,7 @@ export default { window.__UMBRACO_TEST_RUN_A11Y_TEST = ${(!devMode).toString()}; +