Skip to content

Preview segment #19203

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 1, 2025
Merged
7 changes: 7 additions & 0 deletions src/Umbraco.Web.UI.Client/src/apps/preview/apps/manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
101 changes: 81 additions & 20 deletions src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,28 @@

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();
Expand All @@ -30,8 +47,9 @@

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.');
Expand Down Expand Up @@ -75,16 +93,46 @@
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);

Check warning on line 135 in src/Umbraco.Web.UI.Client/src/apps/preview/preview.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (release/16.0)

❌ Getting worse: Complex Method

UmbPreviewContext.setPreviewUrl increases in cyclomatic complexity from 10 to 15, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
}

#setSessionCount(sessions: number) {
Expand Down Expand Up @@ -165,8 +213,9 @@
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;
Expand All @@ -185,20 +234,32 @@
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);

if (mergedArgs.culture) {
params.set('culture', mergedArgs.culture);
} else {
params.delete('culture');
}

this.#setPreviewUrl({ culture: args.culture });
if (mergedArgs.segment) {
params.set('segment', mergedArgs.segment);
} else {
params.delete('segment');
}

if (args.className) wrapper.className = args.className;
if (args.height) wrapper.style.height = args.height;
if (args.width) wrapper.style.width = args.width;
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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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}`);
Expand Down
Loading