Preview segment (#19203)

* add new manifest

* wip element

* add segment to preview context

* add segment icon

* open preview route on the same server

* Update preview.context.ts

* clean up

* pass culture and segment to preview window
This commit is contained in:
Mads Rasmussen
2025-05-01 22:21:02 +02:00
committed by GitHub
parent 04a10c2e8d
commit a201bbd064
4 changed files with 207 additions and 27 deletions

View File

@@ -15,6 +15,13 @@ export const manifests: Array<ManifestPreviewAppProvider> = [
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',

View File

@@ -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<UmbSegmentCollectionItemModel> = [];
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`
<uui-button look="primary" popovertarget="segments-popover">
<div>
<uui-icon name="icon-filter"></uui-icon>
<span>${this._segment?.name ?? 'Segments'}</span>
</div>
</uui-button>
<uui-popover-container id="segments-popover" placement="top-end">
<umb-popover-layout>
<uui-menu-item label="Default" ?active=${!this._segment} @click=${() => this.#onClick()}></uui-menu-item>
${repeat(
this._segments,
(item) => item.unique,
(item) => html`
<uui-menu-item
label=${item.name}
?active=${item.unique === this._segment?.unique}
@click=${() => this.#onClick(item)}>
</uui-menu-item>
`,
)}
</umb-popover-layout>
</uui-popover-container>
`;
}
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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}`);