Media Type
${this._mediaTypeIcon ? html`` : nothing}
@@ -135,6 +149,20 @@ export class UmbMediaWorkspaceViewInfoElement extends UmbLitElement {
`;
}
+ #renderTrashState() {
+ if (!this._isTrashed) return nothing;
+
+ return html`
+
+
+
+ ${this.localize.term('content_trashed')}
+
+
+
+ `;
+ }
+
#renderCreateDate() {
if (!this._createDate) return nothing;
return html`
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts
index 8c0b1ce9aa..07e196850a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time/property-editor-ui-date-time-picker-base.ts
@@ -19,8 +19,8 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
interface UmbDateTime {
- date: string | undefined;
- timeZone: string | undefined;
+ date: string | null;
+ timeZone: string | null;
}
interface UmbTimeZonePickerOption extends UmbTimeZone {
@@ -34,6 +34,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
{
private _timeZoneOptions: Array
= [];
private _clientTimeZone: UmbTimeZone | undefined;
+ private _timeZoneMode: UmbTimeZonePickerValue['mode'] | undefined;
@property({ type: Boolean, reflect: true })
readonly = false;
@@ -104,6 +105,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
() => {
return (
this._displayTimeZone &&
+ this._timeZoneMode !== 'local' &&
!!this.value?.timeZone &&
!this._timeZoneOptions.some((opt) => opt.value === this.value?.timeZone && !opt.invalid)
);
@@ -120,8 +122,13 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
if (this._displayTimeZone) {
timeZonePickerConfig = config.getValueByAlias('timeZones');
}
+
this.#setTimeInputStep(timeFormat);
this.#prefillValue(timeZonePickerConfig);
+
+ // To ensure the expected value matches the prefilled value, we trigger an update.
+ // If the values match, no change event will be fired.
+ this.#updateValue(this._selectedDate?.toISO({ includeOffset: false }) ?? null);
}
#prefillValue(timeZonePickerConfig: UmbTimeZonePickerValue | undefined) {
@@ -158,8 +165,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
#prefillTimeZones(config: UmbTimeZonePickerValue | undefined, selectedDate: DateTime | undefined) {
// Retrieve the time zones from the config
this._clientTimeZone = getClientTimeZone();
+ this._timeZoneMode = config?.mode;
- // Retrieve the time zones from the config
const dateToCalculateOffset = selectedDate ?? DateTime.now();
switch (config?.mode) {
case 'all':
@@ -219,7 +226,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
return;
}
} else if (this.value?.date) {
- return; // If there is a date but no time zone, we don't preselect anything
+ // If there is no time zone in the value, but there is a date, we leave the time zone unselected
+ return;
}
// Check if we can pre-select the client time zone
@@ -269,16 +277,8 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
return;
}
- if (!newPickerValue) {
- this._datePickerValue = '';
- this.value = undefined;
- this._selectedDate = undefined;
- this.dispatchEvent(new UmbChangeEvent());
- return;
- }
-
this._datePickerValue = newPickerValue;
- this.#updateValue(value, true);
+ this.#updateValue(value);
}
#onTimeZoneChange(event: UUIComboboxEvent) {
@@ -291,7 +291,7 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
if (!this._selectedTimeZone) {
if (this.value?.date) {
- this.value = { date: this.value.date, timeZone: undefined };
+ this.value = { date: this.value.date, timeZone: null };
} else {
this.value = undefined;
}
@@ -303,46 +303,84 @@ export abstract class UmbPropertyEditorUiDateTimePickerElementBase
return;
}
- this.#updateValue(this._selectedDate.toISO({ includeOffset: false }) || '');
+ this.#updateValue(this._selectedDate.toISO({ includeOffset: false }));
}
- #updateValue(date: string, updateOffsets = false) {
+ #updateValue(date: string | null) {
// Try to parse the date with the selected time zone
- const newDate = DateTime.fromISO(date, { zone: this._selectedTimeZone ?? 'UTC' });
+ const newDate = date ? DateTime.fromISO(date, { zone: this._selectedTimeZone || 'UTC' }) : null;
// If the date is invalid, we reset the value
- if (!newDate.isValid) {
+ if (!newDate || !newDate.isValid) {
+ if (!this.value) {
+ return; // No change
+ }
this.value = undefined;
this._selectedDate = undefined;
this.dispatchEvent(new UmbChangeEvent());
+ this.#updateOffsets(DateTime.now());
return;
}
+ const previousDate = this._selectedDate;
this._selectedDate = newDate;
- this.value = {
- date: this.#getCurrentDateValue(),
- timeZone: this._selectedTimeZone,
+
+ let timeZoneToStore = null;
+ if (!this._displayTimeZone || !this._timeZoneMode) {
+ timeZoneToStore = null;
+ } else if (this._timeZoneMode === 'local') {
+ timeZoneToStore = 'UTC';
+ } else {
+ timeZoneToStore = this._selectedTimeZone ?? null;
+ }
+
+ const dateToStore =
+ timeZoneToStore && this._selectedTimeZone !== timeZoneToStore ? newDate.setZone(timeZoneToStore) : newDate;
+
+ const newValue = {
+ date: this.#formatDateValue(dateToStore),
+ timeZone: timeZoneToStore,
};
- if (updateOffsets) {
- this._timeZoneOptions.forEach((opt) => {
- opt.offset = getTimeZoneOffset(opt.value, newDate);
- });
- // Update the time zone options (mostly for the offset)
- this._filteredTimeZoneOptions = this._timeZoneOptions;
+ // Only update the stored data if it has actually changed to avoid firing unnecessary change events
+ const previousValue = this.value;
+ if (previousValue?.date === newValue.date && previousValue?.timeZone === newValue.timeZone) {
+ return;
}
+
+ this.value = newValue;
this.dispatchEvent(new UmbChangeEvent());
+
+ // Only update offsets if the date timestamp has changed
+ if (previousDate?.toUnixInteger() !== newDate.toUnixInteger()) {
+ this.#updateOffsets(newDate);
+ }
}
- #getCurrentDateValue(): string | undefined {
+ #updateOffsets(date: DateTime) {
+ if (!this._displayTimeZone) return;
+ this._timeZoneOptions.forEach((opt) => {
+ opt.offset = getTimeZoneOffset(opt.value, date);
+ });
+ // Update the time zone options (mostly for the offset)
+ this._filteredTimeZoneOptions = this._timeZoneOptions;
+ }
+
+ #formatDateValue(date: DateTime): string | null {
+ let formattedDate: string | undefined;
switch (this._dateInputType) {
case 'date':
- return this._selectedDate?.toISODate() ?? undefined;
+ formattedDate = date.toFormat('yyyy-MM-dd');
+ break;
case 'time':
- return this._selectedDate?.toISOTime({ includeOffset: false }) ?? undefined;
+ formattedDate = date.toFormat('HH:mm:ss');
+ break;
default:
- return this._selectedDate?.toISO({ includeOffset: !!this._selectedTimeZone }) ?? undefined;
+ formattedDate = date.toFormat(`yyyy-MM-dd'T'HH:mm:ss${this._timeZoneMode ? 'ZZ' : ''}`);
+ break;
}
+
+ return formattedDate ?? null;
}
#onTimeZoneSearch(event: UUIComboboxEvent) {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts
index 5f7b27b28a..0bde152792 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts
@@ -1,7 +1,7 @@
import { UMB_PARTIAL_VIEW_FROM_SNIPPET_MODAL } from '../snippet-modal/index.js';
import { UMB_PARTIAL_VIEW_FOLDER_REPOSITORY_ALIAS } from '../../../constants.js';
import type { UmbPartialViewCreateOptionsModalData } from './index.js';
-import { html, customElement } from '@umbraco-cms/backoffice/external/lit';
+import { html, customElement, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalBaseElement, umbOpenModal } from '@umbraco-cms/backoffice/modal';
import { UmbCreateFolderEntityAction } from '@umbraco-cms/backoffice/tree';
@@ -62,7 +62,7 @@ export class UmbPartialViewCreateOptionsModalElement extends UmbModalBaseElement
override render() {
return html`
-
+
`;
}
+
+ static override styles = [
+ css`
+ uui-dialog-layout {
+ --uui-menu-item-flat-structure: 1;
+ }
+ `,
+ ];
}
export default UmbPartialViewCreateOptionsModalElement;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/index.ts
index bb3f144fdd..a15bf4f8c0 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/index.ts
@@ -5,8 +5,7 @@ export const UMB_SCRIPT_CREATE_OPTIONS_MODAL = new UmbModalToken
-
+
+
-
- }
-
+
+
-
- }
-
-
+
+
-
+
`;
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts
index 1b13642c84..fae97fe98c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts
@@ -46,7 +46,7 @@ const entityCreateOptionActions: Array = [
meta: {
icon: 'icon-folder',
label: '#create_folder',
- description: '#create_folderDescription',
+ additionalOptions: true,
folderRepositoryAlias: UMB_STYLESHEET_FOLDER_REPOSITORY_ALIAS,
},
},
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar.element.ts
index 0598f61223..3a44356d53 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar.element.ts
@@ -92,11 +92,11 @@ export class UmbTiptapToolbarElement extends UmbLitElement {
}
#renderActions(aliases: Array) {
- return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? this.#renderActionPlaceholder());
+ return repeat(aliases, (alias) => this.#lookup?.get(alias) ?? this.#renderActionPlaceholder(alias));
}
- #renderActionPlaceholder() {
- return html``;
+ #renderActionPlaceholder(alias: string) {
+ return html``;
}
static override readonly styles = css`
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts
index 47523bb921..86ec324126 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/horizontal-rule/manifests.ts
@@ -16,6 +16,7 @@ export const manifests: Array = [
alias: 'Umb.Tiptap.Toolbar.HorizontalRule',
name: 'Horizontal Rule Tiptap Toolbar Extension',
api: () => import('./horizontal-rule.tiptap-toolbar-api.js'),
+ forExtensions: ['Umb.Tiptap.HorizontalRule'],
meta: {
alias: 'horizontalRule',
icon: 'icon-horizontal-rule',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts
index 60c0894e84..b6a98f1ddf 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts
@@ -4,6 +4,7 @@ export const manifests: Array = [
kind: 'styleMenu',
alias: 'Umb.Tiptap.Toolbar.StyleSelect',
name: 'Style Select Tiptap Extension',
+ forExtensions: ['Umb.Tiptap.Heading', 'Umb.Tiptap.Blockquote', 'Umb.Tiptap.CodeBlock'],
items: [
{
label: 'Headers',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/extensions-configuration/property-editor-ui-tiptap-extensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/extensions-configuration/property-editor-ui-tiptap-extensions-configuration.element.ts
index ad71fe5568..89ac81a68f 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/extensions-configuration/property-editor-ui-tiptap-extensions-configuration.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/extensions-configuration/property-editor-ui-tiptap-extensions-configuration.element.ts
@@ -1,14 +1,4 @@
-import {
- css,
- customElement,
- html,
- ifDefined,
- nothing,
- property,
- state,
- repeat,
- when,
-} from '@umbraco-cms/backoffice/external/lit';
+import { css, customElement, html, nothing, property, state, repeat, when } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
@@ -166,7 +156,7 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement
${repeat(
group.extensions,
(item) => html`
-
+
this.#context.removeStatusbarItem([areaIndex, itemIndex])}
- @dragend=${this.#onDragEnd}
- @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [areaIndex, itemIndex])}>
-
- ${when(item.icon, (icon) => html``)}
- ${label}
-
-
- `;
+ switch (item.kind) {
+ case 'unknown':
+ return html`
+ this.#context.removeStatusbarItem([areaIndex, itemIndex])}>
+ `;
+
+ default:
+ return html`
+ this.#context.removeStatusbarItem([areaIndex, itemIndex])}
+ @dragend=${this.#onDragEnd}
+ @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [areaIndex, itemIndex])}>
+
+ ${when(item.icon, (icon) => html``)}
+ ${label}
+
+
+ `;
+ }
}
static override readonly styles = [
@@ -303,8 +317,8 @@ export class UmbPropertyEditorUiTiptapStatusbarConfigurationElement
--color-standalone: var(--uui-color-danger-standalone);
--color-emphasis: var(--uui-color-danger-emphasis);
--color-contrast: var(--uui-color-danger);
- --uui-button-contrast-disabled: var(--uui-color-danger);
- --uui-button-border-color-disabled: var(--uui-color-danger);
+ --uui-button-contrast: var(--uui-color-danger);
+ --uui-button-border-color: var(--uui-color-danger);
}
div {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/tiptap-statusbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/tiptap-statusbar-configuration.context.ts
index 8b3e29cf6e..84844d673f 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/tiptap-statusbar-configuration.context.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/tiptap-statusbar-configuration.context.ts
@@ -31,6 +31,7 @@ export class UmbTiptapStatusbarConfigurationContext extends UmbContextBase {
const _extensions = extensions
.sort((a, b) => a.alias.localeCompare(b.alias))
.map((ext) => ({
+ kind: 'default',
alias: ext.alias,
label: ext.meta.label,
icon: ext.meta.icon,
@@ -75,8 +76,8 @@ export class UmbTiptapStatusbarConfigurationContext extends UmbContextBase {
.filter((ext) => ext.alias?.toLowerCase().includes(query) || ext.label?.toLowerCase().includes(query));
}
- public getExtensionByAlias(alias: string): UmbTiptapStatusbarExtension | undefined {
- return this.#lookup?.get(alias);
+ public getExtensionByAlias(alias: string): UmbTiptapStatusbarExtension {
+ return this.#lookup?.get(alias) ?? { label: '', alias, icon: '', kind: 'unknown' };
}
public isExtensionEnabled(alias: string): boolean {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts
index bb000b6d7c..cebfc2463c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts
@@ -255,9 +255,7 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement
#renderGroup(group?: UmbTiptapToolbarGroupViewModel, rowIndex = 0, groupIndex = 0) {
if (!group) return nothing;
const showActionBar = this._toolbar[rowIndex].data.length > 1 && group.data.length === 0;
- const items: UmbTiptapToolbarExtension[] = group!.data
- .map((alias) => this.#context?.getExtensionByAlias(alias))
- .filter((item): item is UmbTiptapToolbarExtension => !!item);
+ const items = group.data.map((alias) => this.#context?.getExtensionByAlias(alias));
return html`
ext.alias?.toLowerCase().includes(query) || ext.label?.toLowerCase().includes(query));
}
- public getExtensionByAlias(alias: string): UmbTiptapToolbarExtension | undefined {
- return this.#lookup?.get(alias);
+ public getExtensionByAlias(alias: string): UmbTiptapToolbarExtension {
+ return this.#lookup?.get(alias) ?? { label: '', alias, icon: '', kind: 'unknown' };
}
public isExtensionEnabled(alias: string): boolean {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts
index cc7b02d45c..dd8eba4ac8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts
@@ -10,9 +10,9 @@ export class UmbTiptapToolbarGroupConfigurationElement<
TiptapToolbarItem extends UmbTiptapToolbarExtension = UmbTiptapToolbarExtension,
> extends UmbLitElement {
#sorter = new UmbSorterController
(this, {
- getUniqueOfElement: (element) => element.getAttribute('tiptap-toolbar-alias'),
- getUniqueOfModel: (modelEntry) => modelEntry.alias!,
- itemSelector: 'uui-button',
+ getUniqueOfElement: (element) => element.dataset.mark,
+ getUniqueOfModel: (modelEntry) => `tiptap-toolbar-item:${modelEntry.alias}`,
+ itemSelector: '.draggable',
identifier: 'umb-tiptap-toolbar-sorter',
containerSelector: '.items',
resolvePlacement: UmbSorterResolvePlacementAsGrid,
@@ -71,7 +71,7 @@ export class UmbTiptapToolbarGroupConfigurationElement<
}
#renderItem(item: TiptapToolbarItem, index = 0) {
- const label = this.localize.string(item.label);
+ const label = this.localize.string(item.label) || item.alias;
const forbidden = !this.#context?.isExtensionEnabled(item.alias);
switch (item.kind) {
@@ -80,13 +80,11 @@ export class UmbTiptapToolbarGroupConfigurationElement<
return html`
this.#onRequestRemove(item, index)}>
${label}
@@ -95,18 +93,29 @@ export class UmbTiptapToolbarGroupConfigurationElement<
`;
+ case 'unknown':
+ return html`
+
this.#onRequestRemove(item, index)}>
+ `;
+
case 'button':
+ case 'colorPickerButton':
default:
return html`
this.#onRequestRemove(item, index)}>
${when(
@@ -131,23 +140,18 @@ export class UmbTiptapToolbarGroupConfigurationElement<
uui-button {
--uui-button-font-weight: normal;
- &[draggable='true'],
- &[draggable='true'] > .inner {
+ &.draggable,
+ &.draggable > .inner {
cursor: move;
}
- &[disabled],
- &[disabled] > .inner {
- cursor: not-allowed;
- }
-
&.forbidden {
--color: var(--uui-color-danger);
--color-standalone: var(--uui-color-danger-standalone);
--color-emphasis: var(--uui-color-danger-emphasis);
--color-contrast: var(--uui-color-danger);
- --uui-button-contrast-disabled: var(--uui-color-danger);
- --uui-button-border-color-disabled: var(--uui-color-danger);
+ --uui-button-contrast: var(--uui-color-danger);
+ --uui-button-border-color: var(--uui-color-danger);
}
div {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/types.ts
index 303d2f543d..92de056d94 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/types.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/types.ts
@@ -1,17 +1,18 @@
export type UmbTiptapSortableViewModel
= { unique: string; data: T };
-export type UmbTiptapStatusbarExtension = {
+export type UmbTiptapExtensionBase = {
+ kind?: string;
alias: string;
label: string;
icon: string;
dependencies?: Array;
};
+export type UmbTiptapStatusbarExtension = UmbTiptapExtensionBase;
+
export type UmbTiptapStatusbarViewModel = UmbTiptapSortableViewModel>;
-export type UmbTiptapToolbarExtension = UmbTiptapStatusbarExtension & {
- kind?: string;
-};
+export type UmbTiptapToolbarExtension = UmbTiptapExtensionBase;
export type UmbTiptapToolbarRowViewModel = UmbTiptapSortableViewModel>;
export type UmbTiptapToolbarGroupViewModel = UmbTiptapSortableViewModel>;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-granular-permissions.element.ts
similarity index 81%
rename from src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-granular-permissions.element.ts
index 3721b89985..9fb9d37715 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-granular-permissions.element.ts
@@ -2,12 +2,15 @@ import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.contex
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api';
import type { ManifestGranularUserPermission } from '@umbraco-cms/backoffice/user-permission';
-import { html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
+import { html, customElement, state, nothing, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { filterFrozenArray } from '@umbraco-cms/backoffice/observable-api';
-@customElement('umb-user-group-granular-permission-list')
+@customElement('umb-user-group-entity-type-granular-permissions')
export class UmbUserGroupGranularPermissionListElement extends UmbLitElement {
+ @property()
+ public entityType?: string;
+
@state()
private _userGroupPermissions?: Array;
@@ -57,9 +60,21 @@ export class UmbUserGroupGranularPermissionListElement extends UmbLitElement {
override render() {
if (!this._userGroupPermissions) return;
+
+ if (!this.entityType) {
+ return html`
+
+ manifest.forEntityTypes === undefined || manifest.forEntityTypes?.length === 0}
+ .renderMethod=${this.#renderProperty}>
+ `;
+ }
+
return html`
+ manifest.forEntityTypes?.includes(this.entityType!) || manifest.forEntityTypes?.length === 0}
.renderMethod=${this.#renderProperty}>`;
}
@@ -95,6 +110,6 @@ export default UmbUserGroupGranularPermissionListElement;
declare global {
interface HTMLElementTagNameMap {
- 'umb-user-group-granular-permission-list': UmbUserGroupGranularPermissionListElement;
+ 'umb-user-group-entity-type-granular-permissions': UmbUserGroupGranularPermissionListElement;
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permission-groups.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permission-groups.element.ts
new file mode 100644
index 0000000000..2d2d09dff9
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permission-groups.element.ts
@@ -0,0 +1,92 @@
+import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
+import { html, customElement, state, repeat, css, nothing } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
+
+import './user-group-entity-type-permissions.element.js';
+import './user-group-entity-type-granular-permissions.element.js';
+
+@customElement('umb-user-group-entity-type-permission-groups')
+export class UmbUserGroupEntityTypePermissionGroupsElement extends UmbLitElement {
+ @state()
+ private _groups: Array<{ entityType: string; headline: string }> = [];
+
+ @state()
+ private _hasGranularPermissionsWithNoEntityType = false;
+
+ constructor() {
+ super();
+
+ this.observe(
+ umbExtensionsRegistry.byType('entityUserPermission'),
+ (manifests) => {
+ const entityTypes = [...new Set(manifests.flatMap((manifest) => manifest.forEntityTypes))];
+ this._groups = entityTypes
+ .map((entityType) => {
+ return {
+ entityType,
+ headline: this.localize.term(`user_permissionsEntityGroup_${entityType}`),
+ };
+ })
+ .sort((a, b) => a.headline.localeCompare(b.headline));
+ },
+ 'umbUserPermissionsObserver',
+ );
+
+ this.#observeGranularPermissionsWithNoEntityType();
+ }
+
+ #observeGranularPermissionsWithNoEntityType() {
+ this.observe(
+ umbExtensionsRegistry.byTypeAndFilter(
+ 'userGranularPermission',
+ (manifest) => manifest.forEntityTypes === undefined || manifest.forEntityTypes.length === 0,
+ ),
+ (manifests) => {
+ this._hasGranularPermissionsWithNoEntityType = manifests.length > 0;
+ },
+ );
+ }
+
+ override render() {
+ return html`${repeat(
+ this._groups,
+ (group) => group.entityType,
+ (group) =>
+ html`
+ ${group.headline}
+
+
+
+
+ `,
+ )}
+ ${this.#renderUngroupedGranularPermissions()}`;
+ }
+
+ #renderUngroupedGranularPermissions() {
+ if (!this._hasGranularPermissionsWithNoEntityType) return nothing;
+ return html`
+ `;
+ }
+
+ static override styles = [
+ UmbTextStyles,
+ css`
+ uui-box {
+ margin-top: var(--uui-size-space-6);
+ }
+ `,
+ ];
+}
+
+export default UmbUserGroupEntityTypePermissionGroupsElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-user-group-entity-type-permission-groups': UmbUserGroupEntityTypePermissionGroupsElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permissions.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permissions.element.ts
new file mode 100644
index 0000000000..0edd3e9c33
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-type-permissions.element.ts
@@ -0,0 +1,60 @@
+import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context-token.js';
+import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
+import { html, customElement, state, nothing, property } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';
+
+@customElement('umb-user-group-entity-type-permissions')
+export class UmbUserGroupEntityTypePermissionsElement extends UmbLitElement {
+ @property()
+ public entityType?: string;
+
+ @state()
+ private _fallBackPermissions?: Array;
+
+ #userGroupWorkspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE;
+
+ constructor() {
+ super();
+
+ this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => {
+ this.#userGroupWorkspaceContext = instance;
+ this.observe(
+ this.#userGroupWorkspaceContext?.fallbackPermissions,
+ (fallbackPermissions) => {
+ this._fallBackPermissions = fallbackPermissions;
+ },
+ 'umbUserGroupEntityUserPermissionsObserver',
+ );
+ });
+ }
+
+ #onPermissionChange(event: UmbSelectionChangeEvent) {
+ event.stopPropagation();
+ const target = event.target as any;
+ const verbs = target.allowedVerbs;
+ if (verbs === undefined || verbs === null) throw new Error('The verbs are not defined');
+ this.#userGroupWorkspaceContext?.setFallbackPermissions(verbs);
+ }
+
+ override render() {
+ return this.entityType
+ ? html`
+
+ `
+ : nothing;
+ }
+
+ static override styles = [UmbTextStyles];
+}
+
+export default UmbUserGroupEntityTypePermissionsElement;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-user-group-entity-type-permissions': UmbUserGroupEntityTypePermissionsElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-user-permission-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-user-permission-list.element.ts
deleted file mode 100644
index d9ecbdbf19..0000000000
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-entity-user-permission-list.element.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from '../user-group-workspace.context-token.js';
-import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
-import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
-import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event';
-import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
-
-@customElement('umb-user-group-entity-user-permission-list')
-export class UmbUserGroupEntityUserPermissionListElement extends UmbLitElement {
- @state()
- private _fallBackPermissions?: Array;
-
- @state()
- private _groups: Array<{ entityType: string; headline: string }> = [];
-
- #userGroupWorkspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE;
-
- constructor() {
- super();
-
- this.#observeEntityUserPermissions();
-
- this.consumeContext(UMB_USER_GROUP_WORKSPACE_CONTEXT, (instance) => {
- this.#userGroupWorkspaceContext = instance;
- this.observe(
- this.#userGroupWorkspaceContext?.fallbackPermissions,
- (fallbackPermissions) => {
- this._fallBackPermissions = fallbackPermissions;
- },
- 'umbUserGroupEntityUserPermissionsObserver',
- );
- });
- }
-
- #observeEntityUserPermissions() {
- this.observe(
- umbExtensionsRegistry.byType('entityUserPermission'),
- (manifests) => {
- const entityTypes = [...new Set(manifests.flatMap((manifest) => manifest.forEntityTypes))];
- this._groups = entityTypes
- .map((entityType) => {
- return {
- entityType,
- headline: this.localize.term(`user_permissionsEntityGroup_${entityType}`),
- };
- })
- .sort((a, b) => a.headline.localeCompare(b.headline));
- },
- 'umbUserPermissionsObserver',
- );
- }
-
- #onPermissionChange(event: UmbSelectionChangeEvent) {
- event.stopPropagation();
- const target = event.target as any;
- const verbs = target.allowedVerbs;
- if (verbs === undefined || verbs === null) throw new Error('The verbs are not defined');
- this.#userGroupWorkspaceContext?.setFallbackPermissions(verbs);
- }
-
- override render() {
- return html` ${this._groups.map((group) => this.#renderPermissionsForEntityType(group))}`;
- }
-
- #renderPermissionsForEntityType(group: { entityType: string; headline: string }) {
- return html`
- ${group.headline}
-
- `;
- }
-
- static override styles = [UmbTextStyles];
-}
-
-export default UmbUserGroupEntityUserPermissionListElement;
-
-declare global {
- interface HTMLElementTagNameMap {
- 'umb-user-group-default-permission-list': UmbUserGroupEntityUserPermissionListElement;
- }
-}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts
index 701ff856e9..cbbad3824f 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts
@@ -12,8 +12,7 @@ import type { UmbInputLanguageElement } from '@umbraco-cms/backoffice/language';
import { UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/icon';
import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components';
-import './components/user-group-entity-user-permission-list.element.js';
-import './components/user-group-granular-permission-list.element.js';
+import './components/user-group-entity-type-permission-groups.element.js';
@customElement('umb-user-group-workspace-editor')
export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
@@ -33,7 +32,7 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
private _aliasCanBeChanged?: UmbUserGroupDetailModel['aliasCanBeChanged'] = true;
@state()
- private _icon: UmbUserGroupDetailModel['icon'] = null;
+ private _icon?: UmbUserGroupDetailModel['icon'];
@state()
private _sections: UmbUserGroupDetailModel['sections'] = [];
@@ -68,45 +67,44 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
}
#observeUserGroup() {
- if (!this.#workspaceContext) return;
- this.observe(this.#workspaceContext.isNew, (value) => (this._isNew = value), '_observeIsNew');
- this.observe(this.#workspaceContext.unique, (value) => (this._unique = value ?? undefined), '_observeUnique');
- this.observe(this.#workspaceContext.name, (value) => (this._name = value), '_observeName');
- this.observe(this.#workspaceContext.alias, (value) => (this._alias = value), '_observeAlias');
+ this.observe(this.#workspaceContext?.isNew, (value) => (this._isNew = value), '_observeIsNew');
+ this.observe(this.#workspaceContext?.unique, (value) => (this._unique = value ?? undefined), '_observeUnique');
+ this.observe(this.#workspaceContext?.name, (value) => (this._name = value), '_observeName');
+ this.observe(this.#workspaceContext?.alias, (value) => (this._alias = value), '_observeAlias');
this.observe(
- this.#workspaceContext.aliasCanBeChanged,
+ this.#workspaceContext?.aliasCanBeChanged,
(value) => (this._aliasCanBeChanged = value),
'_observeAliasCanBeChanged',
);
- this.observe(this.#workspaceContext.icon, (value) => (this._icon = value), '_observeIcon');
- this.observe(this.#workspaceContext.sections, (value) => (this._sections = value), '_observeSections');
- this.observe(this.#workspaceContext.languages, (value) => (this._languages = value), '_observeLanguages');
+ this.observe(this.#workspaceContext?.icon, (value) => (this._icon = value), '_observeIcon');
+ this.observe(this.#workspaceContext?.sections, (value) => (this._sections = value ?? []), '_observeSections');
+ this.observe(this.#workspaceContext?.languages, (value) => (this._languages = value ?? []), '_observeLanguages');
this.observe(
- this.#workspaceContext.hasAccessToAllLanguages,
- (value) => (this._hasAccessToAllLanguages = value),
+ this.#workspaceContext?.hasAccessToAllLanguages,
+ (value) => (this._hasAccessToAllLanguages = value ?? false),
'_observeHasAccessToAllLanguages',
);
this.observe(
- this.#workspaceContext.documentRootAccess,
- (value) => (this._documentRootAccess = value),
+ this.#workspaceContext?.documentRootAccess,
+ (value) => (this._documentRootAccess = value ?? false),
'_observeDocumentRootAccess',
);
this.observe(
- this.#workspaceContext.documentStartNode,
+ this.#workspaceContext?.documentStartNode,
(value) => (this._documentStartNode = value),
'_observeDocumentStartNode',
);
this.observe(
- this.#workspaceContext.mediaRootAccess,
- (value) => (this._mediaRootAccess = value),
+ this.#workspaceContext?.mediaRootAccess,
+ (value) => (this._mediaRootAccess = value ?? false),
'_observeMediaRootAccess',
);
this.observe(
- this.#workspaceContext.mediaStartNode,
+ this.#workspaceContext?.mediaStartNode,
(value) => (this._mediaStartNode = value),
'_observeMediaStartNode',
);
@@ -242,20 +240,7 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
${this.#renderLanguageAccess()} ${this.#renderDocumentAccess()} ${this.#renderMediaAccess()}
-
-
-
-
-
-
-
-
-
-
-
-
+ ${this.#renderPermissionGroups()}
`;
@@ -337,6 +322,10 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement {
`;
}
+ #renderPermissionGroups() {
+ return html` `;
+ }
+
static override styles = [
UmbTextStyles,
css`
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/components/input-entity-user-permission/input-entity-user-permission.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/components/input-entity-user-permission/input-entity-user-permission.element.ts
index f6fe7c2b9c..5c654b5ed8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/components/input-entity-user-permission/input-entity-user-permission.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/components/input-entity-user-permission/input-entity-user-permission.element.ts
@@ -1,7 +1,7 @@
import type { ManifestEntityUserPermission } from '../../entity-user-permission.extension.js';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
-import { html, customElement, property, state, nothing, ifDefined, css } from '@umbraco-cms/backoffice/external/lit';
+import { html, customElement, property, state, ifDefined, css, repeat } from '@umbraco-cms/backoffice/external/lit';
import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api';
import type { UmbUserPermissionVerbElement } from '@umbraco-cms/backoffice/user';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -24,7 +24,7 @@ export class UmbInputEntityUserPermissionElement extends UmbFormControlMixin(Umb
allowedVerbs: Array = [];
@state()
- private _manifests: Array = [];
+ private _groupedPermissions: Array<[string, ManifestEntityUserPermission[]]> = [];
#manifestObserver?: UmbObserverController>;
@@ -40,11 +40,18 @@ export class UmbInputEntityUserPermissionElement extends UmbFormControlMixin(Umb
this.#manifestObserver?.destroy();
this.#manifestObserver = this.observe(
- umbExtensionsRegistry.byType('entityUserPermission'),
- (userPermissionManifests) => {
- this._manifests = userPermissionManifests.filter((manifest) =>
- manifest.forEntityTypes.includes(this.entityType),
- );
+ umbExtensionsRegistry.byTypeAndFilter('entityUserPermission', (manifest) =>
+ manifest.forEntityTypes.includes(this.entityType),
+ ),
+ (manifests) => {
+ // TODO: groupBy is not known by TS yet
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error
+ const groupedPermissions = Object.groupBy(
+ manifests,
+ (manifest: ManifestEntityUserPermission) => manifest.meta.group,
+ ) as Record>;
+ this._groupedPermissions = Object.entries(groupedPermissions);
},
'umbUserPermissionManifestsObserver',
);
@@ -73,27 +80,14 @@ export class UmbInputEntityUserPermissionElement extends UmbFormControlMixin(Umb
}
override render() {
- return html`${this.#renderGroupedPermissions(this._manifests)} `;
- }
-
- #renderGroupedPermissions(permissionManifests: Array) {
- // TODO: groupBy is not known by TS yet
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error
- const groupedPermissions = Object.groupBy(
- permissionManifests,
- (manifest: ManifestEntityUserPermission) => manifest.meta.group,
- ) as Record>;
- return html`
- ${Object.entries(groupedPermissions).map(
- ([group, manifests]) => html`
- ${group !== 'undefined'
- ? html` ${group}
`
- : nothing}
- ${manifests.map((manifest) => html` ${this.#renderPermission(manifest)} `)}
- `,
- )}
- `;
+ return repeat(this._groupedPermissions, ([group, manifests]) => {
+ const headline = group !== 'undefined' ? `#actionCategories_${group}` : `#actionCategories_general`;
+ return html`
+
+ ${repeat(manifests, (manifest) => html` ${this.#renderPermission(manifest)} `)}
+
+ `;
+ });
}
#renderPermission(manifest: ManifestEntityUserPermission) {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts
index 974200ac95..ffbbf3fc47 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts
@@ -16,5 +16,6 @@ export const UMB_ENTITY_USER_PERMISSION_MODAL = new UmbModalToken<
>('Umb.Modal.EntityUserPermissionSettings', {
modal: {
type: 'sidebar',
+ size: 'medium',
},
});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/user-granular-permission.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/user-granular-permission.extension.ts
index 70801b97b3..5023055136 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/user-granular-permission.extension.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/user-granular-permission.extension.ts
@@ -2,6 +2,7 @@ import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestGranularUserPermission extends ManifestElement {
type: 'userGranularPermission';
+ forEntityTypes?: Array;
meta: MetaGranularUserPermission;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts
index b45660db04..07bfa14320 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/constants.ts
@@ -1,3 +1,4 @@
export * from './views/constants.js';
export const UMB_USER_COLLECTION_ALIAS = 'Umb.Collection.User';
export { UMB_USER_COLLECTION_CONTEXT } from './user-collection.context-token.js';
+export * from './menu/constants.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts
index b54121406d..59374f44b5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/manifests.ts
@@ -1,7 +1,8 @@
import { UMB_USER_COLLECTION_REPOSITORY_ALIAS } from './repository/index.js';
+import { manifests as collectionActionManifests } from './action/manifests.js';
+import { manifests as collectionMenuManifests } from './menu/manifests.js';
import { manifests as collectionRepositoryManifests } from './repository/manifests.js';
import { manifests as collectionViewManifests } from './views/manifests.js';
-import { manifests as collectionActionManifests } from './action/manifests.js';
import { UMB_USER_COLLECTION_ALIAS } from './constants.js';
export const manifests: Array = [
@@ -15,7 +16,8 @@ export const manifests: Array = [
repositoryAlias: UMB_USER_COLLECTION_REPOSITORY_ALIAS,
},
},
+ ...collectionActionManifests,
+ ...collectionMenuManifests,
...collectionRepositoryManifests,
...collectionViewManifests,
- ...collectionActionManifests,
];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts
new file mode 100644
index 0000000000..9b0faca55e
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/constants.ts
@@ -0,0 +1 @@
+export const UMB_USER_COLLECTION_MENU_ALIAS = 'Umb.CollectionMenu.User';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts
new file mode 100644
index 0000000000..4c5ede48d2
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/manifests.ts
@@ -0,0 +1,23 @@
+import { UMB_USER_ENTITY_TYPE } from '../../entity.js';
+import { UMB_USER_COLLECTION_REPOSITORY_ALIAS } from '../repository/constants.js';
+import { UMB_USER_COLLECTION_MENU_ALIAS } from './constants.js';
+
+export const manifests: Array = [
+ {
+ type: 'collectionMenu',
+ kind: 'default',
+ alias: UMB_USER_COLLECTION_MENU_ALIAS,
+ name: 'User Collection Menu',
+ meta: {
+ collectionRepositoryAlias: UMB_USER_COLLECTION_REPOSITORY_ALIAS,
+ },
+ },
+ {
+ type: 'collectionMenuItem',
+ kind: 'default',
+ alias: 'Umb.CollectionMenuItem.User',
+ name: 'User Collection Menu Item',
+ element: () => import('./user-collection-menu-item.element.js'),
+ forEntityTypes: [UMB_USER_ENTITY_TYPE],
+ },
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts
new file mode 100644
index 0000000000..a0ec66d78a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/collection/menu/user-collection-menu-item.element.ts
@@ -0,0 +1,88 @@
+import type { UmbUserDetailModel } from '../../types.js';
+import { html, customElement, css, state, property, nothing } from '@umbraco-cms/backoffice/external/lit';
+import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
+import type { UmbCollectionMenuItemContext } from '@umbraco-cms/backoffice/collection';
+import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
+
+@customElement('umb-user-collection-menu-item')
+export class UmbUserCollectionMenuItemElement extends UmbLitElement {
+ @property({ type: Object, attribute: false })
+ set item(newVal: UmbUserDetailModel) {
+ this._item = newVal;
+
+ if (this._item) {
+ this.#initItem();
+ }
+ }
+ get item(): UmbUserDetailModel | undefined {
+ return this._item;
+ }
+ protected _item?: UmbUserDetailModel;
+
+ @property({ type: Object, attribute: false })
+ public set api(value: UmbCollectionMenuItemContext | undefined) {
+ this.#api = value;
+
+ if (this.#api) {
+ this.observe(this.#api.isSelectable, (value) => (this._isSelectable = value));
+ this.observe(this.#api.isSelected, (value) => (this._isSelected = value));
+ this.#initItem();
+ }
+ }
+ public get api(): UmbCollectionMenuItemContext | undefined {
+ return this.#api;
+ }
+ #api: UmbCollectionMenuItemContext | undefined;
+
+ @state()
+ protected _isActive = false;
+
+ @state()
+ protected _isSelected = false;
+
+ @state()
+ protected _isSelectable = false;
+
+ #initItem() {
+ if (!this.#api) return;
+ if (!this._item) return;
+ this.#api.setItem(this._item);
+ }
+
+ override render() {
+ const item = this._item;
+ if (!item) return nothing;
+
+ return html`
+ this.#api?.select()}
+ @deselected=${() => this.#api?.deselect()}>
+
+
+ `;
+ }
+
+ static override styles = [
+ UmbTextStyles,
+ css`
+ umb-user-avatar {
+ font-size: 10px;
+ }
+ `,
+ ];
+}
+
+export { UmbUserCollectionMenuItemElement as element };
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'umb-user-collection-menu-item': UmbUserCollectionMenuItemElement;
+ }
+}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts
index 03273e88df..ec1100d7a6 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/manifests.ts
@@ -3,7 +3,6 @@ export const manifests: Array = [
type: 'modal',
alias: 'Umb.Modal.User.Picker',
name: 'User Picker Modal',
- js: () => import('./user-picker/user-picker-modal.element.js'),
},
{
type: 'modal',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts
deleted file mode 100644
index e79f930e88..0000000000
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.element.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { UmbUserCollectionRepository } from '../../collection/repository/user-collection.repository.js';
-import type { UmbUserItemModel } from '../../repository/item/index.js';
-import type { UmbUserPickerModalData, UmbUserPickerModalValue } from './user-picker-modal.token.js';
-import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
-import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
-import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
-import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
-import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
-
-@customElement('umb-user-picker-modal')
-export class UmbUserPickerModalElement extends UmbModalBaseElement {
- @state()
- private _users: Array = [];
-
- #selectionManager = new UmbSelectionManager(this);
- #userCollectionRepository = new UmbUserCollectionRepository(this);
-
- override connectedCallback(): void {
- super.connectedCallback();
-
- // TODO: in theory this config could change during the lifetime of the modal, so we could observe it
- this.#selectionManager.setMultiple(this.data?.multiple ?? false);
- this.#selectionManager.setSelection(this.value?.selection ?? []);
- }
-
- protected override firstUpdated(_changedProperties: PropertyValueMap | Map): void {
- super.firstUpdated(_changedProperties);
- this.#requestUsers();
- }
-
- async #requestUsers() {
- if (!this.#userCollectionRepository) return;
- const { data } = await this.#userCollectionRepository.requestCollection();
-
- if (data) {
- this._users = data.items;
- }
- }
-
- #submit() {
- this.value = { selection: this.#selectionManager.getSelection() };
- this.modalContext?.submit();
- }
-
- #close() {
- this.modalContext?.reject();
- }
-
- override render() {
- return html`
-
-
- ${this._users.map(
- (user) => html`
- this.#selectionManager.select(user.unique)}
- @deselected=${() => this.#selectionManager.deselect(user.unique)}
- ?selected=${this.#selectionManager.isSelected(user.unique)}>
-
-
- `,
- )}
-
-
-
-
-
-
- `;
- }
-
- static override styles = [
- UmbTextStyles,
- css`
- umb-user-avatar {
- font-size: 12px;
- }
- `,
- ];
-}
-
-export default UmbUserPickerModalElement;
-
-declare global {
- interface HTMLElementTagNameMap {
- 'umb-user-picker-modal': UmbUserPickerModalElement;
- }
-}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts
index a72fc779c8..2f2e030b6c 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/modals/user-picker/user-picker-modal.token.ts
@@ -1,19 +1,29 @@
import type { UmbUserDetailModel } from '../../types.js';
-import type { UmbPickerModalData } from '@umbraco-cms/backoffice/modal';
+import { UMB_USER_COLLECTION_MENU_ALIAS } from '../../collection/constants.js';
+import type {
+ UmbCollectionItemPickerModalData,
+ UmbCollectionItemPickerModalValue,
+} from '@umbraco-cms/backoffice/collection';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
-export type UmbUserPickerModalData = UmbPickerModalData;
+export type UmbUserPickerModalData = UmbCollectionItemPickerModalData;
-export interface UmbUserPickerModalValue {
- selection: Array;
-}
+// eslint-disable-next-line @typescript-eslint/no-empty-object-type
+export interface UmbUserPickerModalValue extends UmbCollectionItemPickerModalValue {}
export const UMB_USER_PICKER_MODAL = new UmbModalToken(
- 'Umb.Modal.User.Picker',
+ /* TODO: use constant. We had to use the string directly here to avoid a circular dependency.
+ When we have removed the dataType (dependency on content) from the picker context we update this */
+ 'Umb.Modal.CollectionItemPicker',
{
modal: {
type: 'sidebar',
size: 'small',
},
+ data: {
+ collection: {
+ menuAlias: UMB_USER_COLLECTION_MENU_ALIAS,
+ },
+ },
},
);
diff --git a/src/Umbraco.Web.UI/Program.cs b/src/Umbraco.Web.UI/Program.cs
index ad68d28351..a9834ed784 100644
--- a/src/Umbraco.Web.UI/Program.cs
+++ b/src/Umbraco.Web.UI/Program.cs
@@ -25,9 +25,6 @@ app.UseUmbraco()
})
.WithEndpoints(u =>
{
- /*#if (UmbracoRelease = 'LTS')
- u.UseInstallerEndpoints();
- #endif */
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
index 051ecc5cfd..2ade2ca396 100644
--- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
+++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
@@ -26,6 +26,11 @@
+
+
diff --git a/templates/UmbracoProject/.template.config/starterkits.template.json b/templates/UmbracoProject/.template.config/starterkits.template.json
index 745eef20ae..4dbb28c005 100644
--- a/templates/UmbracoProject/.template.config/starterkits.template.json
+++ b/templates/UmbracoProject/.template.config/starterkits.template.json
@@ -34,11 +34,11 @@
"cases": [
{
"condition": "(StarterKit == 'Umbraco.TheStarterKit' && (UmbracoRelease == 'Latest' || UmbracoRelease == 'Custom'))",
- "value": "16.0.0"
+ "value": "17.0.0-rc"
},
{
"condition": "(StarterKit == 'Umbraco.TheStarterKit' && UmbracoRelease == 'LTS')",
- "value": "13.0.0"
+ "value": "17.0.0"
}
]
}
diff --git a/templates/UmbracoProject/Dockerfile b/templates/UmbracoProject/Dockerfile
index 40347ec2ec..9f2fcbe193 100644
--- a/templates/UmbracoProject/Dockerfile
+++ b/templates/UmbracoProject/Dockerfile
@@ -26,8 +26,5 @@ USER root
RUN mkdir umbraco
RUN mkdir umbraco/Logs
RUN chown $APP_UID umbraco --recursive
-#if (UmbracoRelease = 'LTS')
-RUN chown $APP_UID wwwroot/umbraco --recursive
-#endif
USER $APP_UID
ENTRYPOINT ["dotnet", "UmbracoProject.dll"]
diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
index 87ed32ef3a..4b057b1d03 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
+++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
@@ -8,7 +8,7 @@
"hasInstallScript": true,
"dependencies": {
"@umbraco/json-models-builders": "^2.0.40",
- "@umbraco/playwright-testhelpers": "^17.0.0-beta.4",
+ "@umbraco/playwright-testhelpers": "^17.0.0-beta.7",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"node-fetch": "^2.6.7"
@@ -58,21 +58,21 @@
}
},
"node_modules/@umbraco/json-models-builders": {
- "version": "2.0.40",
- "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.40.tgz",
- "integrity": "sha512-Yqojp/0akRgXsnjg18+MjMdkRvFrmlUNbfITgZ3d1h/PIRbWXPNKY1YAfZmdUv+g1SRSHrbIRpPPtSy+gNOjHw==",
+ "version": "2.0.41",
+ "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.41.tgz",
+ "integrity": "sha512-rCNUHCOpcuWIj7xUhk0lpcn4jzk9y82jHs9FSb7kxH716AnDyYvwuI+J0Ayd4hhWtXXqNCRqugCNYjG+rvzshQ==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.1"
}
},
"node_modules/@umbraco/playwright-testhelpers": {
- "version": "17.0.0-beta.4",
- "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.0-beta.4.tgz",
- "integrity": "sha512-+OE1A2oAdFel4myf5T/jJLuw0aLvSOUBplkUfsYFj2ACeLygfAp/MM7q2RQ+YlCym/wdF+jAqJM3g+zsKEDjaQ==",
+ "version": "17.0.0-beta.7",
+ "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-17.0.0-beta.7.tgz",
+ "integrity": "sha512-5fhmVVSpJkH6Inx8nA9qqqvZzYuPdDxJdQF2IzY0oSf8C0eti+TJ2BKrYfTLmZTfVqmHUas72BMGser5pfpl9A==",
"license": "MIT",
"dependencies": {
- "@umbraco/json-models-builders": "2.0.40",
+ "@umbraco/json-models-builders": "2.0.41",
"node-fetch": "^2.6.7"
}
},
diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json
index f1a7b85fa4..013bfdd438 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/package.json
+++ b/tests/Umbraco.Tests.AcceptanceTest/package.json
@@ -22,7 +22,7 @@
},
"dependencies": {
"@umbraco/json-models-builders": "^2.0.40",
- "@umbraco/playwright-testhelpers": "^17.0.0-beta.4",
+ "@umbraco/playwright-testhelpers": "^17.0.0-beta.7",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"node-fetch": "^2.6.7"
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts
index 65437ebcac..ec7d32041d 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDocumentTypeProperties/ContentWithAllowedChildNodes.spec.ts
@@ -51,7 +51,7 @@ test('cannot create child content if allowed child node is disabled', async ({um
// Assert
await umbracoUi.content.isDocumentTypeNameVisible(documentTypeName, false);
- await umbracoUi.content.doesModalHaveText(noAllowedDocumentTypeAvailableMessage);
+ await umbracoUi.content.doesDocumentModalHaveText(noAllowedDocumentTypeAvailableMessage);
});
test('can create multiple child nodes with different document types', async ({umbracoApi, umbracoUi}) => {
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CreateContentFromDocumentBlueprint.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CreateContentFromDocumentBlueprint.spec.ts
index 7ec3a9223f..0b3bf06cf0 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CreateContentFromDocumentBlueprint.spec.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/CreateContentFromDocumentBlueprint.spec.ts
@@ -51,7 +51,7 @@ test('can create content using an invariant document blueprint', async ({umbraco
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
- await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
+ await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName);
await umbracoUi.content.clickSaveButtonForContent();
// Assert
@@ -75,7 +75,7 @@ test('can create content using a variant document blueprint', async ({umbracoApi
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
- await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
+ await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName);
await umbracoUi.content.clickSaveButtonForContent();
await umbracoUi.content.clickSaveButton();
@@ -104,7 +104,7 @@ test('can create content with different name using an invariant document bluepri
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
- await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
+ await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickSaveButtonForContent();
@@ -130,7 +130,7 @@ test('can create content with different name using a variant document blueprint'
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
- await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
+ await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickSaveButtonForContent();
await umbracoUi.content.clickSaveButton();
@@ -161,7 +161,7 @@ test('can create content using a document blueprint with block list', async ({um
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
- await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
+ await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName);
await umbracoUi.content.clickSaveButtonForContent();
// Assert
@@ -187,7 +187,7 @@ test('can create content using a document blueprint with block grid', async ({um
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
- await umbracoUi.content.clickModalMenuItemWithName(documentBlueprintName);
+ await umbracoUi.content.selectDocumentBlueprintWithName(documentBlueprintName);
await umbracoUi.content.clickSaveButtonForContent();
// Assert
@@ -197,4 +197,4 @@ test('can create content using a document blueprint with block grid', async ({um
expect(contentData.values[0].value.contentData[0].values[0].value.markup).toEqual(textContent);
const blockListValue = contentData.values.find(item => item.editorAlias === "Umbraco.BlockGrid")?.value;
expect(blockListValue).toBeTruthy();
-});
\ No newline at end of file
+});
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts
index fdcef65d63..f9e7ce5a72 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/DocumentBlueprint/DocumentBlueprint.spec.ts
@@ -23,7 +23,9 @@ test('can create a document blueprint from the settings menu', {tag: '@smoke'},
// Act
await umbracoUi.documentBlueprint.clickActionsMenuAtRoot();
await umbracoUi.documentBlueprint.clickCreateActionMenuOption();
+ await umbracoUi.documentBlueprint.clickCreateNewDocumentBlueprintButton();
await umbracoUi.documentBlueprint.clickTextButtonWithName(documentTypeName);
+ await umbracoUi.documentBlueprint.clickChooseButton();
await umbracoUi.documentBlueprint.enterDocumentBlueprintName(documentBlueprintName);
await umbracoUi.documentBlueprint.clickSaveButton();
@@ -108,7 +110,9 @@ test('can create a variant document blueprint', {tag: '@release'}, async ({umbra
// Act
await umbracoUi.documentBlueprint.clickActionsMenuAtRoot();
await umbracoUi.documentBlueprint.clickCreateActionMenuOption();
+ await umbracoUi.documentBlueprint.clickCreateNewDocumentBlueprintButton();
await umbracoUi.documentBlueprint.clickTextButtonWithName(documentTypeName);
+ await umbracoUi.documentBlueprint.clickChooseButton();
await umbracoUi.documentBlueprint.enterDocumentBlueprintName(documentBlueprintName);
await umbracoUi.documentBlueprint.clickSaveButton();
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts
index 650c7d09d6..51544486d5 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/Webhook.spec.ts
@@ -173,7 +173,7 @@ test('can remove a header from a webhook', async ({umbracoApi, umbracoUi}) => {
expect(await umbracoApi.webhook.doesWebhookHaveHeader(webhookName, headerName, headerValue)).toBeFalsy();
});
-test('cannot add both content event and media event for a webhook', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => {
+test('cannot add both content event and media event for a webhook', async ({umbracoApi, umbracoUi}) => {
// Arrange
const event = 'Content Published';
await umbracoApi.webhook.createDefaultWebhook(webhookName, webhookSiteToken, event);
@@ -185,4 +185,4 @@ test('cannot add both content event and media event for a webhook', {tag: '@rele
// Assert
await umbracoUi.webhook.isModalMenuItemWithNameDisabled('Media Saved');
await umbracoUi.webhook.isModalMenuItemWithNameDisabled('Media Deleted');
-});
\ No newline at end of file
+});
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/WebhookTrigger.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/WebhookTrigger.spec.ts
index 51dc9ede2d..9d4f1c68a2 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/WebhookTrigger.spec.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Webhook/WebhookTrigger.spec.ts
@@ -27,7 +27,7 @@ test.afterEach(async ({umbracoApi}) => {
await umbracoApi.media.ensureNameNotExists(mediaName);
});
-test('can trigger when content is published', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => {
+test('can trigger when content is published', async ({umbracoApi, umbracoUi}) => {
test.slow();
// Arrange
diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs
index 86e1508801..fdf02388a9 100644
--- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs
+++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs
@@ -34,9 +34,7 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat
protected ContentCreateModel Textpage { get; private set; }
- protected ContentScheduleCollection ContentSchedule { get; private set; }
-
- protected CultureAndScheduleModel CultureAndSchedule { get; private set; }
+ protected ICollection CultureAndSchedule { get; private set; }
protected int TextpageId { get; private set; }
@@ -91,11 +89,7 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat
}
// Sets the culture and schedule for the content, in this case, we are publishing immediately for all cultures
- ContentSchedule = new ContentScheduleCollection();
- CultureAndSchedule = new CultureAndScheduleModel
- {
- CulturesToPublishImmediately = new HashSet { "*" }, Schedules = ContentSchedule,
- };
+ CultureAndSchedule = [new CulturePublishScheduleModel { Culture = "*", Schedule = null }];
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054
PublishedTextPage = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Published Page");
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs
index f051aa1557..6739487317 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/PublishedContent/PublishedContentFallbackTests.cs
@@ -35,6 +35,8 @@ public class PublishedContentFallbackTests : UmbracoIntegrationTest
private IApiContentBuilder ApiContentBuilder => GetRequiredService();
+ private ILanguageService LanguageService => GetRequiredService();
+
protected override void CustomTestSetup(IUmbracoBuilder builder)
=> builder
.AddUmbracoHybridCache()
@@ -98,6 +100,36 @@ public class PublishedContentFallbackTests : UmbracoIntegrationTest
Assert.AreEqual(invariantTitle, invariantValue);
}
+ [TestCase("Danish title", true)]
+ [TestCase("Danish title", false)]
+ [TestCase(null, true)]
+ [TestCase(null, false)]
+ public async Task Property_Value_Can_Perform_Explicit_Language_Fallback(string? danishTitle, bool performFallbackToDefaultLanguage)
+ {
+ var danishLanguage = new Language("da-DK", "Danish")
+ {
+ FallbackIsoCode = "en-US"
+ };
+ await LanguageService.CreateAsync(danishLanguage, Constants.Security.SuperUserKey);
+
+ UmbracoContextFactory.EnsureUmbracoContext();
+
+ const string englishTitle = "English title";
+ var publishedContent = await SetupCultureVariantContentAsync(englishTitle, danishTitle);
+
+ VariationContextAccessor.VariationContext = new VariationContext(culture: "da-DK", segment: null);
+ var danishValue = publishedContent.Value(PublishedValueFallback, "title");
+ Assert.AreEqual(danishTitle ?? string.Empty, danishValue);
+
+ var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage;
+ var fallbackValue = publishedContent.Value(PublishedValueFallback, "title", fallback: fallback);
+ Assert.AreEqual(danishTitle ?? englishTitle, fallbackValue);
+
+ VariationContextAccessor.VariationContext = new VariationContext(culture: "en-US", segment: null);
+ var englishValue = publishedContent.Value(PublishedValueFallback, "title");
+ Assert.AreEqual(englishTitle, englishValue);
+ }
+
private async Task SetupSegmentedContentAsync(string? invariantTitle, string? segmentedTitle)
{
var contentType = new ContentTypeBuilder()
@@ -124,11 +156,47 @@ public class PublishedContentFallbackTests : UmbracoIntegrationTest
ContentService.Save(content);
ContentService.Publish(content, ["*"]);
+ return GetPublishedContent(content.Key);
+ }
+
+ private async Task SetupCultureVariantContentAsync(string englishTitle, string? danishTitle)
+ {
+ var contentType = new ContentTypeBuilder()
+ .WithAlias("theContentType")
+ .WithContentVariation(ContentVariation.Culture)
+ .AddPropertyType()
+ .WithAlias("title")
+ .WithName("Title")
+ .WithDataTypeId(Constants.DataTypes.Textbox)
+ .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox)
+ .WithValueStorageType(ValueStorageType.Nvarchar)
+ .WithVariations(ContentVariation.Culture)
+ .Done()
+ .WithAllowAsRoot(true)
+ .Build();
+ await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+
+ var content = new ContentBuilder()
+ .WithContentType(contentType)
+ .WithCultureName("en-US", "EN")
+ .WithCultureName("da-DK", "DA")
+ .WithName("Content")
+ .Build();
+ content.SetValue("title", englishTitle, culture: "en-US");
+ content.SetValue("title", danishTitle, culture: "da-DK");
+ ContentService.Save(content);
+ ContentService.Publish(content, ["en-US", "da-DK"]);
+
+ return GetPublishedContent(content.Key);
+ }
+
+ private IPublishedContent GetPublishedContent(Guid key)
+ {
ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]);
UmbracoContextAccessor.Clear();
var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext;
- var publishedContent = umbracoContext.Content.GetById(content.Key);
+ var publishedContent = umbracoContext.Content.GetById(key);
Assert.IsNotNull(publishedContent);
return publishedContent;
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs
index 48e225045d..db57a313e8 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs
@@ -1932,4 +1932,95 @@ internal partial class BlockListElementLevelVariationTests
Assert.AreEqual(expectedPickedContent.Key, actualPickedPublishedContent.Key);
}
}
+
+ [TestCase(ContentVariation.Culture, false)]
+ [TestCase(ContentVariation.Culture, true)]
+ [TestCase(ContentVariation.Nothing, false)]
+ [TestCase(ContentVariation.Nothing, true)]
+ public async Task Can_Perform_Language_Fallback(ContentVariation elementTypeVariation, bool performFallbackToDefaultLanguage)
+ {
+ var daDkLanguage = await LanguageService.GetAsync("da-DK");
+ Assert.IsNotNull(daDkLanguage);
+ daDkLanguage.FallbackIsoCode = "en-US";
+ var saveLanguageResult = await LanguageService.UpdateAsync(daDkLanguage, Constants.Security.SuperUserKey);
+ Assert.IsTrue(saveLanguageResult.Success);
+
+ daDkLanguage = await LanguageService.GetAsync("da-DK");
+ Assert.AreEqual("en-US", daDkLanguage?.FallbackIsoCode);
+
+ var elementType = CreateElementType(elementTypeVariation);
+ var blockListDataType = await CreateBlockListDataType(elementType);
+ var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture);
+
+ var content = CreateContent(
+ contentType,
+ elementType,
+ new []
+ {
+ new BlockProperty(
+ new List
+ {
+ new() { Alias = "invariantText", Value = "English invariantText content value" },
+ new() { Alias = "variantText", Value = "English variantText content value" }
+ },
+ new List
+ {
+ new() { Alias = "invariantText", Value = "English invariantText settings value" },
+ new() { Alias = "variantText", Value = "English variantText settings value" }
+ },
+ "en-US",
+ null)
+ },
+ true);
+
+ AssertPropertyValuesWithFallback("en-US",
+ "English invariantText content value", "English variantText content value",
+ "English invariantText settings value", "English variantText settings value");
+
+ AssetEmptyPropertyValues("da-DK");
+
+ AssertPropertyValuesWithFallback("da-DK",
+ "English invariantText content value", "English variantText content value",
+ "English invariantText settings value", "English variantText settings value");
+
+ void AssertPropertyValuesWithFallback(string culture,
+ string expectedInvariantContentValue, string expectedVariantContentValue,
+ string expectedInvariantSettingsValue, string expectedVariantSettingsValue)
+ {
+ SetVariationContext(culture, null);
+ var publishedContent = GetPublishedContent(content.Key);
+
+ var fallback = performFallbackToDefaultLanguage ? Fallback.ToDefaultLanguage : Fallback.ToLanguage;
+
+ var publishedValueFallback = GetRequiredService();
+ var value = publishedContent.Value(publishedValueFallback, "blocks", fallback: fallback);
+ Assert.IsNotNull(value);
+ Assert.AreEqual(1, value.Count);
+
+ var blockListItem = value.First();
+ Assert.AreEqual(2, blockListItem.Content.Properties.Count());
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText"));
+ Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText"));
+ });
+
+ Assert.AreEqual(2, blockListItem.Settings.Properties.Count());
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(expectedInvariantSettingsValue, blockListItem.Settings.Value("invariantText"));
+ Assert.AreEqual(expectedVariantSettingsValue, blockListItem.Settings.Value("variantText"));
+ });
+ }
+
+ void AssetEmptyPropertyValues(string culture)
+ {
+ SetVariationContext(culture, null);
+ var publishedContent = GetPublishedContent(content.Key);
+
+ var value = publishedContent.Value("blocks");
+ Assert.NotNull(value);
+ Assert.IsEmpty(value);
+ }
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs
index 7a1a5d0dac..48f8a035be 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTimePropertyEditorTests.cs
@@ -8,7 +8,6 @@ using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
-using Umbraco.Cms.Infrastructure.HybridCache;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
@@ -39,10 +38,10 @@ public class DateTimePropertyEditorTests : UmbracoIntegrationTest
private static readonly object[] _sourceList1 =
[
- new object[] { Constants.PropertyEditors.Aliases.DateOnly, false, new DateOnly(2025, 1, 22) },
+ new object[] { Constants.PropertyEditors.Aliases.DateOnly, false, new DateOnly(2025, 6, 22) },
new object[] { Constants.PropertyEditors.Aliases.TimeOnly, false, new TimeOnly(18, 33, 1) },
- new object[] { Constants.PropertyEditors.Aliases.DateTimeUnspecified, false, new DateTime(2025, 1, 22, 18, 33, 1) },
- new object[] { Constants.PropertyEditors.Aliases.DateTimeWithTimeZone, true, new DateTimeOffset(2025, 1, 22, 18, 33, 1, TimeSpan.Zero) },
+ new object[] { Constants.PropertyEditors.Aliases.DateTimeUnspecified, false, new DateTime(2025, 6, 22, 18, 33, 1) },
+ new object[] { Constants.PropertyEditors.Aliases.DateTimeWithTimeZone, true, new DateTimeOffset(2025, 6, 22, 18, 33, 1, TimeSpan.FromHours(2)) },
];
[TestCaseSource(nameof(_sourceList1))]
@@ -106,7 +105,7 @@ public class DateTimePropertyEditorTests : UmbracoIntegrationTest
.WithValue(
new JsonObject
{
- ["date"] = "2025-01-22T18:33:01.0000000+00:00",
+ ["date"] = "2025-06-22T18:33:01.0000000+02:00",
["timeZone"] = "Europe/Copenhagen",
})
.Done()
@@ -127,7 +126,6 @@ public class DateTimePropertyEditorTests : UmbracoIntegrationTest
Assert.IsTrue(publishResult.Success);
- var test = ((DocumentCache)PublishedContentCache).GetAtRoot(false);
var publishedContent = await PublishedContentCache.GetByIdAsync(createContentResult.Result.Content.Key, false);
Assert.IsNotNull(publishedContent);
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
index 1aec16e4a0..45d192587f 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs
@@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
@@ -26,10 +27,10 @@ internal sealed class DocumentHybridCacheTests : UmbracoIntegrationTestWithConte
private IPublishedContentCache PublishedContentHybridCache => GetRequiredService();
- private IContentEditingService ContentEditingService => GetRequiredService();
-
private IContentPublishingService ContentPublishingService => GetRequiredService();
+ private IDocumentCacheService DocumentCacheService => GetRequiredService();
+
private const string NewName = "New Name";
private const string NewTitle = "New Title";
@@ -467,6 +468,61 @@ internal sealed class DocumentHybridCacheTests : UmbracoIntegrationTestWithConte
Assert.IsNull(textPage);
}
+ [Test]
+ public async Task Can_Get_Published_Content_By_Id_After_Previous_Check_Where_Not_Found()
+ {
+ // Arrange
+ var testPageKey = Guid.NewGuid();
+
+ // Act & Assert
+ // - assert we cannot get the content that doesn't yet exist from the cache
+ var testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey);
+ Assert.IsNull(testPage);
+
+ testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey);
+ Assert.IsNull(testPage);
+
+ // - create and publish the content
+ var testPageContent = ContentEditingBuilder.CreateBasicContent(ContentType.Key, testPageKey);
+ var createResult = await ContentEditingService.CreateAsync(testPageContent, Constants.Security.SuperUserKey);
+ Assert.IsTrue(createResult.Success);
+ var publishResult = await ContentPublishingService.PublishAsync(testPageKey, CultureAndSchedule, Constants.Security.SuperUserKey);
+ Assert.IsTrue(publishResult.Success);
+
+ // - assert we can now get the content from the cache
+ testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey);
+ Assert.IsNotNull(testPage);
+ }
+
+ [Test]
+ public async Task Can_Get_Published_Content_By_Id_After_Previous_Exists_Check()
+ {
+ // Act
+ var hasContentForTextPageCached = await DocumentCacheService.HasContentByIdAsync(PublishedTextPageId);
+ Assert.IsTrue(hasContentForTextPageCached);
+ var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId);
+
+ // Assert
+ AssertPublishedTextPage(textPage);
+ }
+
+ [Test]
+ public async Task Can_Do_Exists_Check_On_Created_Published_Content()
+ {
+ var testPageKey = Guid.NewGuid();
+ var testPageContent = ContentEditingBuilder.CreateBasicContent(ContentType.Key, testPageKey);
+ var createResult = await ContentEditingService.CreateAsync(testPageContent, Constants.Security.SuperUserKey);
+ Assert.IsTrue(createResult.Success);
+ var publishResult = await ContentPublishingService.PublishAsync(testPageKey, CultureAndSchedule, Constants.Security.SuperUserKey);
+ Assert.IsTrue(publishResult.Success);
+
+ var testPage = await PublishedContentHybridCache.GetByIdAsync(testPageKey);
+ Assert.IsNotNull(testPage);
+
+ var hasContentForTextPageCached = await DocumentCacheService.HasContentByIdAsync(testPage.Id);
+ Assert.IsTrue(hasContentForTextPageCached);
+ }
+
private void AssertTextPage(IPublishedContent textPage)
{
Assert.Multiple(() =>
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs
index 1461364d02..38c233e2b9 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs
@@ -1,11 +1,7 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
-using System;
-using System.Collections.Generic;
using System.Globalization;
-using System.Linq;
-using System.Threading;
using Microsoft.Extensions.Primitives;
using NUnit.Framework;
using Umbraco.Cms.Core.PropertyEditors;
@@ -31,6 +27,17 @@ public class ObjectExtensionsTests
private CultureInfo _savedCulture;
+ [Test]
+ public void Can_Create_Enumerable_Of_One()
+ {
+ var input = "hello";
+#pragma warning disable CS0618 // Type or member is obsolete
+ var result = input.AsEnumerableOfOne();
+#pragma warning restore CS0618 // Type or member is obsolete
+ Assert.AreEqual(1, result.Count());
+ Assert.AreEqual("hello", result.First());
+ }
+
[Test]
public void Can_Convert_List_To_Enumerable()
{
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs
index dd7395f5d0..1f38c034f2 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeUnspecifiedValueConverterTests.cs
@@ -86,7 +86,7 @@ public class DateTimeUnspecifiedValueConverterTests
private static object[] _dateTimeUnspecifiedConvertToObjectCases =
[
new object[] { null, null },
- new object[] { _convertToObjectInputDate, DateTime.Parse("2025-08-20T17:30:00") },
+ new object[] { _convertToObjectInputDate, DateTime.Parse("2025-08-20T16:30:00") },
];
[TestCaseSource(nameof(_dateTimeUnspecifiedConvertToObjectCases))]
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs
index a0bcd82ec9..ee3da51671 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/TimeOnlyValueConverterTests.cs
@@ -86,7 +86,7 @@ public class TimeOnlyValueConverterTests
private static object[] _timeOnlyConvertToObjectCases =
[
new object[] { null, null },
- new object[] { _convertToObjectInputDate, TimeOnly.Parse("17:30") },
+ new object[] { _convertToObjectInputDate, TimeOnly.Parse("16:30") },
];
[TestCaseSource(nameof(_timeOnlyConvertToObjectCases))]
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs
index 152fe28b4e..8da30cd118 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/Extensions/HybridCacheExtensionsTests.cs
@@ -1,3 +1,4 @@
+using System;
using Microsoft.Extensions.Caching.Hybrid;
using Moq;
using NUnit.Framework;
@@ -33,15 +34,15 @@ public class HybridCacheExtensionsTests
_cacheMock
.Setup(cache => cache.GetOrCreateAsync(
key,
- null!,
- It.IsAny>>(),
+ It.IsAny>>(),
+ It.IsAny>, CancellationToken, ValueTask>>(),
It.IsAny(),
null,
CancellationToken.None))
.ReturnsAsync(expectedValue);
// Act
- var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key);
+ var exists = await HybridCacheExtensions.ExistsAsync(_cacheMock.Object, key, CancellationToken.None);
// Assert
Assert.IsTrue(exists);
@@ -56,24 +57,24 @@ public class HybridCacheExtensionsTests
_cacheMock
.Setup(cache => cache.GetOrCreateAsync(
key,
- null!,
- It.IsAny>>(),
+ It.IsAny>>(),
+ It.IsAny>, CancellationToken, ValueTask>>(),
It.IsAny(),
null,
CancellationToken.None))
.Returns((
string key,
- object? state,
- Func