diff --git a/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/login.jpg b/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/login.jpg index ed893bf3c0..9908e3649e 100644 Binary files a/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/login.jpg and b/src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/assets/login.jpg differ diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index e2b349e2d0..e446f3b644 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -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/*" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 3df656ff68..25bd5f11f3 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "16.0.0-rc", + "version": "16.1.0-rc", "type": "module", "exports": { ".": null, 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/mocks/handlers/backoffice/assets/login.jpg b/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/assets/login.jpg index ed893bf3c0..9908e3649e 100644 Binary files a/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/assets/login.jpg and b/src/Umbraco.Web.UI.Client/src/mocks/handlers/backoffice/assets/login.jpg differ diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-area-type-permission/block-grid-area-type-permission.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-area-type-permission/block-grid-area-type-permission.element.ts index 4db70b0ef4..c829c22721 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-area-type-permission/block-grid-area-type-permission.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-area-type-permission/block-grid-area-type-permission.element.ts @@ -29,7 +29,8 @@ export class UmbPropertyEditorUIBlockGridAreaTypePermissionElement @state() private _value: Array = []; - _blockTypes: Array = []; + @state() + private _blockTypes?: Array; @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`
${repeat( this._value, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts index e9082ad7b5..9d518bd362 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts @@ -170,7 +170,7 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement 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; 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}`); diff --git a/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts b/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts index 730423de71..e1fd1f4bce 100644 --- a/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts +++ b/src/Umbraco.Web.UI.Login/src/components/layouts/auth-layout.element.ts @@ -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, diff --git a/version.json b/version.json index 88a6be4a03..ed9f5c15e9 100644 --- a/version.json +++ b/version.json @@ -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" },