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 index 70b6c62c48..b9c2126a96 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/manifests.ts @@ -15,6 +15,13 @@ export const manifests: Array = [ element: () => import('./preview-culture.element.js'), weight: 300, }, + { + type: 'previewApp', + alias: 'Umb.PreviewApps.Segment', + name: 'Preview: Segment Switcher', + element: () => import('./preview-segment.element.js'), + weight: 290, + }, { type: 'previewApp', alias: 'Umb.PreviewApps.OpenWebsite', diff --git a/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-segment.element.ts b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-segment.element.ts new file mode 100644 index 0000000000..3e2974daf4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/apps/preview-segment.element.ts @@ -0,0 +1,108 @@ +import { UMB_PREVIEW_CONTEXT } from '../preview.context.js'; +import { + css, + customElement, + html, + nothing, + repeat, + state, + type PropertyValues, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbSegmentCollectionRepository, type UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment'; + +@customElement('umb-preview-segment') +export class UmbPreviewSegmentElement extends UmbLitElement { + #segmentRepository = new UmbSegmentCollectionRepository(this); + + @state() + private _segment?: UmbSegmentCollectionItemModel; + + @state() + private _segments: Array = []; + + protected override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.#loadSegments(); + } + + async #loadSegments() { + const { data } = await this.#segmentRepository.requestCollection({ skip: 0, take: 100 }); + this._segments = data?.items ?? []; + + const searchParams = new URLSearchParams(window.location.search); + const segment = searchParams.get('segment'); + + if (segment && segment !== this._segment?.unique) { + this._segment = this._segments.find((c) => c.unique === segment); + } + } + + async #onClick(segment?: UmbSegmentCollectionItemModel) { + if (this._segment === segment) return; + this._segment = segment; + + const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT); + previewContext?.updateIFrame({ segment: segment?.unique }); + } + + override render() { + if (this._segments.length <= 1) return nothing; + return html` + +
+ + ${this._segment?.name ?? 'Segments'} +
+
+ + + this.#onClick()}> + ${repeat( + this._segments, + (item) => item.unique, + (item) => html` + this.#onClick(item)}> + + `, + )} + + + `; + } + + static override 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 { UmbPreviewSegmentElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-preview-segment': UmbPreviewSegmentElement; + } +} 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 index 014ed0cc31..21899e41f8 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts @@ -8,11 +8,28 @@ import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'; const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions'; +interface UmbPreviewIframeArgs { + className?: string; + culture?: string; + height?: string; + segment?: string; + width?: string; +} + +interface UmbPreviewUrlArgs { + culture?: string | null; + rnd?: number; + segment?: string | null; + serverUrl?: string; + unique?: string | null; +} + export class UmbPreviewContext extends UmbContextBase { + #unique?: string | null; #culture?: string | null; + #segment?: string | null; #serverUrl: string = ''; #webSocket?: WebSocket; - #unique?: string | null; #iframeReady = new UmbBooleanState(false); public readonly iframeReady = this.#iframeReady.asObservable(); @@ -30,8 +47,9 @@ export class UmbPreviewContext extends UmbContextBase { const params = new URLSearchParams(window.location.search); - this.#culture = params.get('culture'); this.#unique = params.get('id'); + this.#culture = params.get('culture'); + this.#segment = params.get('segment'); if (!this.#unique) { console.error('No unique ID found in query string.'); @@ -75,16 +93,46 @@ export class UmbPreviewContext extends UmbContextBase { return Math.max(Number(localStorage.getItem(UMB_LOCALSTORAGE_SESSION_KEY)), 0) || 0; } - #setPreviewUrl(args?: { serverUrl?: string; unique?: string | null; culture?: string | null; rnd?: number }) { + #setPreviewUrl(args?: UmbPreviewUrlArgs) { const host = args?.serverUrl || this.#serverUrl; - const path = args?.unique || this.#unique; - const params = new URLSearchParams(); + const unique = args?.unique || this.#unique; + + if (!unique) { + throw new Error('No unique ID found in query string.'); + } + + const url = new URL(unique, host); + const params = new URLSearchParams(url.search); + const culture = args?.culture || this.#culture; + const segment = args?.segment || this.#segment; - if (culture) params.set('culture', culture); - if (args?.rnd) params.set('rnd', args.rnd.toString()); + const cultureParam = 'culture'; + const rndParam = 'rnd'; + const segmentParam = 'segment'; - this.#previewUrl.setValue(`${host}/${path}?${params}`); + if (culture) { + params.set(cultureParam, culture); + } else { + params.delete(cultureParam); + } + + if (args?.rnd) { + params.set(rndParam, args.rnd.toString()); + } else { + params.delete(rndParam); + } + + if (segment) { + params.set(segmentParam, segment); + } else { + params.delete(segmentParam); + } + + const previewUrl = new URL(url.pathname + '?' + params.toString(), host); + const previewUrlString = previewUrl.toString(); + + this.#previewUrl.setValue(previewUrlString); } #setSessionCount(sessions: number) { @@ -165,8 +213,9 @@ export class UmbPreviewContext extends UmbContextBase { this.#setSessionCount(sessions); } - async updateIFrame(args?: { culture?: string; className?: string; height?: string; width?: string }) { - if (!args) return; + #currentArgs: UmbPreviewIframeArgs = {}; + async updateIFrame(args?: UmbPreviewIframeArgs) { + const mergedArgs = { ...this.#currentArgs, ...args }; const wrapper = this.getIFrameWrapper(); if (!wrapper) return; @@ -185,20 +234,32 @@ export class UmbPreviewContext extends UmbContextBase { window.addEventListener('resize', scaleIFrame); wrapper.addEventListener('transitionend', scaleIFrame); - if (args.culture) { - this.#iframeReady.setValue(false); + 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); + const params = new URLSearchParams(window.location.search); - this.#setPreviewUrl({ culture: args.culture }); + if (mergedArgs.culture) { + params.set('culture', mergedArgs.culture); + } else { + params.delete('culture'); } - if (args.className) wrapper.className = args.className; - if (args.height) wrapper.style.height = args.height; - if (args.width) wrapper.style.width = args.width; + if (mergedArgs.segment) { + params.set('segment', mergedArgs.segment); + } else { + params.delete('segment'); + } + + const newRelativePathQuery = window.location.pathname + '?' + params.toString(); + history.pushState(null, '', newRelativePathQuery); + + this.#currentArgs = mergedArgs; + + this.#setPreviewUrl({ culture: mergedArgs.culture, segment: mergedArgs.segment }); + + if (mergedArgs.className) wrapper.className = mergedArgs.className; + if (mergedArgs.height) wrapper.style.height = mergedArgs.height; + if (mergedArgs.width) wrapper.style.width = mergedArgs.width; } } 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 e7a9f9bf72..56781134cd 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 @@ -20,7 +20,7 @@ import { UmbDocumentValidationRepository } from '../repository/validation/index. import { UMB_DOCUMENT_CONFIGURATION_CONTEXT } from '../index.js'; import { UMB_DOCUMENT_DETAIL_MODEL_VARIANT_SCAFFOLD, UMB_DOCUMENT_WORKSPACE_ALIAS } from './constants.js'; import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; -import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { type UmbPublishableWorkspaceContext, UmbWorkspaceIsNewRedirectController, @@ -362,13 +362,13 @@ export class UmbDocumentWorkspaceContext const unique = this.getUnique(); if (!unique) throw new Error('Unique is missing'); - let culture = UMB_INVARIANT_CULTURE; + let firstVariantId = UmbVariantId.CreateInvariant(); // Save document (the active variant) before previewing. const { selected } = await this._determineVariantOptions(); if (selected.length > 0) { - culture = selected[0]; - const variantIds = [UmbVariantId.FromString(culture)]; + firstVariantId = UmbVariantId.FromString(selected[0]); + const variantIds = [firstVariantId]; const saveData = await this._data.constructData(variantIds); await this.runMandatoryValidationForSaveData(saveData); await this.performCreateOrUpdate(variantIds, saveData); @@ -383,11 +383,15 @@ export class UmbDocumentWorkspaceContext } const backofficePath = serverContext.getBackofficePath(); - const previewUrl = new URL(ensurePathEndsWithSlash(backofficePath) + 'preview', serverContext.getServerUrl()); + const previewUrl = new URL(ensurePathEndsWithSlash(backofficePath) + 'preview', window.location.origin); previewUrl.searchParams.set('id', unique); - if (culture && culture !== UMB_INVARIANT_CULTURE) { - previewUrl.searchParams.set('culture', culture); + if (firstVariantId.culture) { + previewUrl.searchParams.set('culture', firstVariantId.culture); + } + + if (firstVariantId.segment) { + previewUrl.searchParams.set('segment', firstVariantId.segment); } const previewWindow = window.open(previewUrl.toString(), `umbpreview-${unique}`);