Merge branch 'v16/dev' into contrib

This commit is contained in:
Jacob Overgaard
2025-05-05 14:16:22 +02:00
13 changed files with 231 additions and 44 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 254 KiB

View File

@@ -1,12 +1,12 @@
{
"name": "@umbraco-cms/backoffice",
"version": "16.0.0-rc",
"version": "16.1.0-rc",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@umbraco-cms/backoffice",
"version": "16.0.0-rc",
"version": "16.1.0-rc",
"license": "MIT",
"workspaces": [
"./src/packages/*"

View File

@@ -1,7 +1,7 @@
{
"name": "@umbraco-cms/backoffice",
"license": "MIT",
"version": "16.0.0-rc",
"version": "16.1.0-rc",
"type": "module",
"exports": {
".": null,

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 254 KiB

View File

@@ -29,7 +29,8 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement
@state()
private _value: Array<UmbBlockGridTypeAreaTypePermission> = [];
_blockTypes: Array<UmbBlockTypeWithGroupKey> = [];
@state()
private _blockTypes?: Array<UmbBlockTypeWithGroupKey>;
@state()
private _blockTypesWithElementName: Array<{ type: UmbBlockTypeWithGroupKey; name: string }> = [];
@@ -48,7 +49,7 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement
this.observe(this.#itemsManager.items, (items) => {
this._blockTypesWithElementName = items
.map((item) => {
const blockType = this._blockTypes.find((block) => block.contentElementTypeKey === item.unique);
const blockType = this._blockTypes?.find((block) => block.contentElementTypeKey === item.unique);
if (blockType) {
return { type: blockType, name: item.name };
}
@@ -123,9 +124,12 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement
}
override render() {
if (this._blockTypesWithElementName.length === 0) {
if (this._blockTypes === undefined) {
return nothing;
}
if (this._blockTypesWithElementName.length === 0) {
return 'There must be one Block Type created before permissions can be configured.';
}
return html`<div id="permissions">
${repeat(
this._value,

View File

@@ -170,7 +170,7 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement<UmbModalAppAuthC
css`
:host {
display: block;
background: var(--uui-color-surface, #f4f4f4);
background: var(--uui-color-background, #f4f4f4);
--curves-color: var(--umb-login-curves-color, #f5c1bc);
--curves-display: var(--umb-login-curves-display, inline);
@@ -204,13 +204,13 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement<UmbModalAppAuthC
}
#curve-top {
top: 0px;
right: 0px;
top: -9%;
right: -9%;
}
#curve-bottom {
bottom: 0px;
left: 0px;
bottom: -0.5%;
left: -0.1%;
}
#content-container {

View File

@@ -151,7 +151,10 @@ export class UmbInputDocumentTypeElement extends UmbFormControlMixin<string | un
#getPickableFilter() {
if (this.documentTypesOnly) {
return (x: UmbDocumentTypeTreeItemModel) => x.isFolder === false && x.isElement === false;
/* TODO: We do not have the same model in the tree and during the search, so theoretically, we cannot use the same filter.
The search item model does not include "isFolder," so it checks for falsy intentionally.
We need to investigate getting this typed correctly. [MR] */
return (x: UmbDocumentTypeTreeItemModel) => !x.isFolder && x.isElement === false;
}
if (this.elementTypesOnly) {
return (x: UmbDocumentTypeTreeItemModel) => x.isElement;

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

View File

@@ -178,13 +178,13 @@ export class UmbAuthLayoutElement extends UmbLitElement {
}
#curve-top {
top: 0;
right: 0;
top: -9%;
right: -9%;
}
#curve-bottom {
bottom: 0;
left: 0;
bottom: -0.5%;
left: -0.1%;
}
#logo-on-image,

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "16.0.0-rc2",
"version": "16.1.0-rc",
"assemblyVersion": {
"precision": "build"
},