+
Date: Mon, 11 Aug 2025 16:02:06 +0200
Subject: [PATCH 19/86] Remove unwanted character
---
.../property-editor-ui-date-with-time-zone-picker.element.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
index cceae211844f..71e65643eb85 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
@@ -316,7 +316,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
if (this._timeZoneOptions.length === 1) {
return html`${this._timeZoneOptions[0].name} ${this._timeZoneOptions[0].value ===
this._clientTimeZone?.value
- ? ` (${this.localize.term('timeZonePicker_local')}})`
+ ? ` (${this.localize.term('timeZonePicker_local')})`
: nothing}`;
}
From 598ac5cfe4f6179db29c313502112e89ed5a84a4 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Mon, 11 Aug 2025 16:29:16 +0200
Subject: [PATCH 20/86] Fix incorrect order of custom time zones list
---
.../packages/core/utils/date/date.timezone.ts | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/date/date.timezone.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/date/date.timezone.ts
index 1dcfaab7d27e..ab64488233c5 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/date/date.timezone.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/date/date.timezone.ts
@@ -16,17 +16,20 @@ export function getTimeZoneList(
filter: Array | undefined = undefined,
selectedDate: DateTime | undefined = undefined,
): Array {
+ if (filter) {
+ return filter.map((tz) => ({
+ value: tz,
+ name: getTimeZoneName(tz),
+ offset: getTimeZoneOffset(tz, selectedDate ?? DateTime.now()),
+ }));
+ }
+
const timeZones = Intl.supportedValuesOf('timeZone')
// Exclude offset time zones, e.g. 'Etc/GMT+2', as they are not consistent between browsers
- .filter(
- (value) =>
- value !== 'UTC' && !value.startsWith('Etc/') && (!filter || filter.some((f) => isEquivalentTimeZone(f, value))),
- );
+ .filter((value) => value !== 'UTC' && !value.startsWith('Etc/'));
- if (!filter || filter.includes('UTC')) {
- // Add UTC to the top of the list
- timeZones.unshift('UTC');
- }
+ // Add UTC to the top of the list
+ timeZones.unshift('UTC');
const date = selectedDate ?? DateTime.now();
return timeZones.map((tz) => ({
From 630e5074ae27d9c990966ccedfa7271e8348ee0c Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 12 Aug 2025 10:48:39 +0200
Subject: [PATCH 21/86] Small fixes (mostly validation)
---
.../input-time-zone-item.element.ts | 12 ++-
.../input-time-zone-picker.element.ts | 42 +++-------
.../input-time-zone.element.ts | 82 +++++--------------
...erty-editor-ui-time-zone-picker.element.ts | 16 ++--
4 files changed, 48 insertions(+), 104 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-item.element.ts
index ee10243a9b15..292a7736de73 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-item.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-item.element.ts
@@ -1,6 +1,5 @@
import { html, customElement, css, property, nothing, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { getTimeZoneName } from '@umbraco-cms/backoffice/utils';
@@ -8,7 +7,10 @@ import { getTimeZoneName } from '@umbraco-cms/backoffice/utils';
* @element umb-input-time-zone-item
*/
@customElement('umb-input-time-zone-item')
-export class UmbInputTimeZoneItemElement extends UUIFormControlMixin(UmbLitElement, '') {
+export class UmbInputTimeZoneItemElement extends UmbLitElement {
+ @property({ reflect: false })
+ value = '';
+
/**
* Disables the input
* @type {boolean}
@@ -27,12 +29,8 @@ export class UmbInputTimeZoneItemElement extends UUIFormControlMixin(UmbLitEleme
@property({ type: Boolean, reflect: true })
readonly = false;
- protected override getFormElement() {
- return undefined;
- }
-
override render() {
- const label = getTimeZoneName(this.value as string);
+ const label = getTimeZoneName(this.value);
return html`
${this.disabled || this.readonly ? nothing : html`
`}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-picker.element.ts
index 8425fe7dc316..240f8eed8084 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-picker.element.ts
@@ -11,14 +11,13 @@ import {
} from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
-import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { TimeZone } from '@umbraco-cms/backoffice/utils';
/**
* @element umb-input-time-zone-picker
*/
@customElement('umb-input-time-zone-picker')
-export class UmbInputTimeZonePickerElement extends UUIFormControlMixin(UmbLitElement, '') {
+export class UmbInputTimeZonePickerElement extends UmbLitElement {
/**
* Disables the input
* @type {boolean}
@@ -37,7 +36,7 @@ export class UmbInputTimeZonePickerElement extends UUIFormControlMixin(UmbLitEle
@property({ type: Boolean, reflect: true })
readonly = false;
- @property({ type: Array, reflect: true })
+ @property({ type: Array })
public set options(value) {
this.#options = value;
this._filteredOptions = value;
@@ -75,25 +74,11 @@ export class UmbInputTimeZonePickerElement extends UUIFormControlMixin(UmbLitEle
);
}
- // Prevent valid events from bubbling outside the message element
- #onValid(event: Event) {
- event.stopPropagation();
- }
-
- // Prevent invalid events from bubbling outside the message element
- #onInvalid(event: Event) {
- event.stopPropagation();
- }
-
public override async focus() {
await this.updateComplete;
this._input?.focus();
}
- protected override getFormElement() {
- return undefined;
- }
-
#renderTimeZoneOption = (option: Option) =>
html`
${option.name}
@@ -101,25 +86,22 @@ export class UmbInputTimeZonePickerElement extends UUIFormControlMixin(UmbLitEle
override render() {
return html`
-
-
- ${until(repeat(this._filteredOptions, this.#renderTimeZoneOption))}
-
-
+
+ ${until(repeat(this._filteredOptions, this.#renderTimeZoneOption))}
+
${when(
!this.readonly,
() => html`
,
* @default undefined
*/
@property({ type: Number })
- min?: number;
-
- /**
- * Min validation message.
- * @type {boolean}
- * @attr
- * @default
- */
- @property({ type: String, attribute: 'min-message' })
- minMessage = 'This field need more items';
+ public set min(value) {
+ this.#min = value;
+ }
+ public get min() {
+ if (this.required && this.#min < 1) {
+ return 1;
+ }
+ return this.#min;
+ }
+ #min = 0;
/**
* This is a maximum amount of selected items in this input.
@@ -60,15 +59,6 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
@property({ type: Number })
max?: number;
- /**
- * Max validation message.
- * @type {boolean}
- * @attr
- * @default
- */
- @property({ type: String, attribute: 'min-message' })
- maxMessage = 'This field exceeds the allowed amount of items';
-
/**
* Disables the input
* @type {boolean}
@@ -112,9 +102,6 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
@property({ type: Boolean })
required?: boolean;
- @property({ type: String })
- requiredMessage?: string;
-
@property({ type: Array, reflect: false })
override set value(value: Array) {
super.value = value;
@@ -131,25 +118,23 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
super();
this._timeZoneList = getTimeZoneList(undefined, DateTime.now());
- this.addValidator(
- 'valueMissing',
- () => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
- () => !this.readonly && !!this.required && (!this.value || this.value.length === 0),
- );
-
this.addValidator(
'rangeUnderflow',
- () => this.minMessage,
- () => !!this.min && this.value.length < this.min,
+ () => this.localize.term('validation_entriesShort', this.min, this.min - this.value.length),
+ () => this.value.length < this.min,
);
this.addValidator(
'rangeOverflow',
- () => this.maxMessage,
+ () => this.localize.term('validation_entriesExceed', this.max, this.value.length - (this.max || 0)),
() => !!this.max && this.value.length > this.max,
);
}
+ protected override getFormElement() {
+ return undefined;
+ }
+
#onAdd(event: UmbTimeZoneAddEvent) {
this.value = [...this.value, event.getValue()];
this.pristine = false;
@@ -157,7 +142,6 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
}
#onChange(event: UmbChangeEvent, currentIndex: number) {
- event.stopPropagation();
const target = event.currentTarget as UmbInputTimeZoneItemElement;
const value = target.value as string;
this.value = this.value.map((item, index) => (index === currentIndex ? value : item));
@@ -165,35 +149,15 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
this.dispatchEvent(new UmbChangeEvent());
}
- #deleteItem(event: UmbDeleteEvent, itemIndex: number) {
- event.stopPropagation();
+ #deleteItem(itemIndex: number) {
this.value = this.value.filter((_item, index) => index !== itemIndex);
this.pristine = false;
this.dispatchEvent(new UmbChangeEvent());
}
- // Prevent valid events from bubbling outside the message element
- #onValid(event: Event) {
- event.stopPropagation();
- }
-
- // Prevent invalid events from bubbling outside the message element
- #onInvalid(event: Event) {
- event.stopPropagation();
- }
-
- override getFormElement() {
- return undefined;
- }
-
override render() {
- return html`
- ${this.#renderSelectedItems()}
- ${this.#renderAddTimeZone()}
- `;
+ return html`${this.#renderSelectedItems()}
+ ${this.#renderAddTimeZone()}`;
}
#renderSelectedItems() {
@@ -205,13 +169,11 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
this.#deleteItem(event, index)}
+ @delete=${() => this.#deleteItem(index)}
@change=${(event: UmbChangeEvent) => this.#onChange(event, index)}>
`,
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
index 9952bde734ea..5462d8713000 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
@@ -106,13 +106,15 @@ export class UmbPropertyEditorUITimeZonePickerElement extends UmbLitElement impl
value="custom">
-
-
+
+
+
+
`;
}
From 7eb3960011b2c39d201b8d9615dfe26edbe5206c Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 12 Aug 2025 10:55:08 +0200
Subject: [PATCH 22/86] Rename input time zone component
---
src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts | 2 +-
.../components/{time-zone-input => input-time-zone}/index.ts | 0
.../input-time-zone-item.element.ts | 0
.../input-time-zone-picker.element.ts | 0
.../input-time-zone.element.ts | 0
5 files changed, 1 insertion(+), 1 deletion(-)
rename src/Umbraco.Web.UI.Client/src/packages/core/components/{time-zone-input => input-time-zone}/index.ts (100%)
rename src/Umbraco.Web.UI.Client/src/packages/core/components/{time-zone-input => input-time-zone}/input-time-zone-item.element.ts (100%)
rename src/Umbraco.Web.UI.Client/src/packages/core/components/{time-zone-input => input-time-zone}/input-time-zone-picker.element.ts (100%)
rename src/Umbraco.Web.UI.Client/src/packages/core/components/{time-zone-input => input-time-zone}/input-time-zone.element.ts (100%)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts
index 894224d6fc40..5b6d1429be39 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts
@@ -19,6 +19,7 @@ export * from './input-manifest/index.js';
export * from './input-number-range/index.js';
export * from './input-radio-button-list/index.js';
export * from './input-slider/index.js';
+export * from './input-time-zone/index.js';
export * from './input-toggle/index.js';
export * from './input-with-alias/input-with-alias.element.js';
export * from './multiple-color-picker-input/index.js';
@@ -28,4 +29,3 @@ export * from './ref-item/index.js';
export * from './stack/index.js';
export * from './split-panel/index.js';
export * from './table/index.js';
-export * from './time-zone-input/index.js';
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/index.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/index.ts
rename to src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/index.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-item.element.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-item.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-item.element.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone-picker.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
similarity index 100%
rename from src/Umbraco.Web.UI.Client/src/packages/core/components/time-zone-input/input-time-zone.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
From 10ad08b98cba2219500b862427db40f64b51fd89 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 12 Aug 2025 12:14:38 +0200
Subject: [PATCH 23/86] Small adjustments
---
.../input-time-zone-picker.element.ts | 85 +++++--------------
.../input-time-zone.element.ts | 76 +++++++++++++----
...erty-editor-ui-time-zone-picker.element.ts | 2 +-
3 files changed, 80 insertions(+), 83 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
index 240f8eed8084..27e9ddf1cc2a 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
@@ -1,17 +1,8 @@
-import {
- html,
- customElement,
- css,
- property,
- query,
- when,
- until,
- repeat,
- state,
-} from '@umbraco-cms/backoffice/external/lit';
+import { html, customElement, css, property, query, until, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
import type { TimeZone } from '@umbraco-cms/backoffice/utils';
+import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
/**
* @element umb-input-time-zone-picker
@@ -46,6 +37,15 @@ export class UmbInputTimeZonePickerElement extends UmbLitElement {
}
#options: Array = [];
+ @property({ type: String, reflect: true })
+ public set value(value) {
+ this.#value = value;
+ }
+ public get value() {
+ return this.#value;
+ }
+ #value: string = '';
+
@state()
private _filteredOptions: Array = [];
@@ -60,11 +60,9 @@ export class UmbInputTimeZonePickerElement extends UmbLitElement {
this._filteredOptions = this.options;
}
- async #onAdd() {
- const input = this._input;
- if (typeof input?.value !== 'string') return;
- this.dispatchEvent(new UmbTimeZoneAddEvent({ value: input.value }));
- input.value = '';
+ public override async focus() {
+ await this.updateComplete;
+ this._input?.focus();
}
#onSearch(event: UUIComboboxEvent) {
@@ -74,9 +72,9 @@ export class UmbInputTimeZonePickerElement extends UmbLitElement {
);
}
- public override async focus() {
- await this.updateComplete;
- this._input?.focus();
+ #onChange(event: UUIComboboxEvent) {
+ this.value = ((event.currentTarget as UUIComboboxElement)?.value as string) ?? '';
+ this.dispatchEvent(new UmbChangeEvent());
}
#renderTimeZoneOption = (option: Option) =>
@@ -90,70 +88,25 @@ export class UmbInputTimeZonePickerElement extends UmbLitElement {
id="input"
pristine
label="${this.localize.term('general_value')} ${this._input?.value}"
+ .value=${this.value}
@search="${this.#onSearch}"
+ @change="${this.#onChange}"
?disabled=${this.disabled}
?readonly=${this.readonly}>
${until(repeat(this._filteredOptions, this.#renderTimeZoneOption))}
-
- ${when(
- !this.readonly,
- () => html`
-
-
-
- `,
- )}
`;
}
static override styles = [
css`
- :host {
- display: flex;
- margin-bottom: var(--uui-size-space-3);
- gap: var(--uui-size-space-1);
- }
-
#input {
width: 100%;
- --uui-input-height: var(--uui-size-12);
- }
-
- #validation-message {
- flex: 1;
- }
-
- .handle {
- cursor: grab;
- }
-
- .handle:active {
- cursor: grabbing;
}
`,
];
}
-export class UmbTimeZoneAddEvent extends Event {
- #value: string;
-
- public constructor({ value }: { value: string }) {
- super('added', { bubbles: true, composed: true });
- this.#value = value;
- }
-
- public getValue() {
- return this.#value;
- }
-}
-
export default UmbInputTimeZonePickerElement;
declare global {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
index ea9e2c11ebc1..5db2f80efb41 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
@@ -1,6 +1,16 @@
import type UmbInputTimeZoneItemElement from './input-time-zone-item.element.js';
-import type { UmbTimeZoneAddEvent } from './input-time-zone-picker.element.js';
-import { html, customElement, property, css, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
+import type { UmbInputTimeZonePickerElement } from './input-time-zone-picker.element.js';
+import {
+ html,
+ customElement,
+ property,
+ css,
+ repeat,
+ nothing,
+ query,
+ when,
+ state,
+} from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
@@ -36,7 +46,6 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
* This is a minimum amount of selected items in this input.
* @type {number}
* @attr
- * @default undefined
*/
@property({ type: Number })
public set min(value) {
@@ -54,7 +63,6 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
* This is a maximum amount of selected items in this input.
* @type {number}
* @attr
- * @default undefined
*/
@property({ type: Number })
max?: number;
@@ -112,6 +120,12 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
return super.value;
}
+ @state()
+ private _disableAddButton = true;
+
+ @query('umb-input-time-zone-picker')
+ protected _timeZonePicker?: UmbInputTimeZonePickerElement;
+
private _timeZoneList: Array = [];
constructor() {
@@ -135,8 +149,12 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
return undefined;
}
- #onAdd(event: UmbTimeZoneAddEvent) {
- this.value = [...this.value, event.getValue()];
+ #onAdd() {
+ if (this._timeZonePicker) {
+ this.value = [...this.value, this._timeZonePicker.value];
+ this._timeZonePicker.value = '';
+ this._disableAddButton = true;
+ }
this.pristine = false;
this.dispatchEvent(new UmbChangeEvent());
}
@@ -172,7 +190,6 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
value=${item}
?disabled=${this.disabled}
?readonly=${this.readonly}
- @enter=${this.#onAdd}
@delete=${() => this.#deleteItem(index)}
@change=${(event: UmbChangeEvent) => this.#onChange(event, index)}>
@@ -184,20 +201,47 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
#renderAddTimeZone() {
if (this.disabled || this.readonly) return nothing;
return html`
- !this.value.includes(tz.value))}
- ?disabled=${this.disabled}
- ?readonly=${this.readonly}
- @added=${this.#onAdd}>
-
+
+ !this.value.includes(tz.value))}
+ @change=${(event: UmbChangeEvent) => {
+ this._disableAddButton = !(event.target as UmbInputTimeZonePickerElement)?.value;
+ }}
+ ?disabled=${this.disabled}
+ ?readonly=${this.readonly}>
+
+ ${when(
+ !this.readonly,
+ () => html`
+
+
+
+ `,
+ )}
+
`;
}
static override styles = [
css`
- #action {
- display: block;
+ #add-time-zone {
+ display: flex;
+ margin-bottom: var(--uui-size-space-3);
+ gap: var(--uui-size-space-1);
+ }
+
+ #time-zone-picker {
+ width: 100%;
+ display: inline-flex;
+ --uui-input-height: var(--uui-size-12);
}
.--umb-sorter-placeholder {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
index 5462d8713000..57e9810baa9b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
@@ -85,7 +85,7 @@ export class UmbPropertyEditorUITimeZonePickerElement extends UmbLitElement impl
return html`
Date: Tue, 12 Aug 2025 14:56:31 +0200
Subject: [PATCH 24/86] Using model for stored value
---
.../DateTimeWithTimeZonePropertyEditor.cs | 61 +++++++++++---
...meWithTimeZonePropertyIndexValueFactory.cs | 13 ++-
.../DateWithTimeZoneConfiguration.cs | 54 ++++++++++++-
.../DateTimeWithTimeZoneValueConverter.cs | 80 +++++++++++++++----
4 files changed, 178 insertions(+), 30 deletions(-)
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
index 30851db2eed7..02baaef8985c 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
@@ -49,6 +49,7 @@ protected override IDataValueEditor CreateValueEditor() =>
private class DateTimeWithTimeZoneDataValueEditor : DataValueEditor
{
+ private readonly IJsonSerializer _jsonSerializer;
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
public DateTimeWithTimeZoneDataValueEditor(
@@ -60,6 +61,7 @@ public DateTimeWithTimeZoneDataValueEditor(
IDataTypeConfigurationCache dataTypeConfigurationCache)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
+ _jsonSerializer = jsonSerializer;
_dataTypeConfigurationCache = dataTypeConfigurationCache;
Validators.AddRange(new DateTimeWithTimeZoneValidator(localizedTextService));
}
@@ -90,34 +92,52 @@ public DateTimeWithTimeZoneDataValueEditor(
dateTimeOffset = new DateTimeOffset(DateOnly.MinValue, TimeOnly.FromTimeSpan(dateTimeOffset.TimeOfDay), dateTimeOffset.Offset);
}
- valueAsJsonObject["date"] = dateTimeOffset;
- return editorValue.Value;
+ var value = new DateTimeWithTimeZoneValueConverter.DateWithTimeZone
+ {
+ Date = dateTimeOffset,
+ TimeZone = valueAsJsonObject["timeZone"]?.GetValue(),
+ };
+
+ var jsonStr = _jsonSerializer.Serialize(value);
+ return jsonStr;
}
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var value = property.GetValue(culture, segment);
- if (value is not string valueString || JsonNode.Parse(valueString) is not JsonObject valueAsJsonObject)
+ if (value is not string valueString)
+ {
+ return null;
+ }
+
+ var interValue = DateTimeWithTimeZoneValueConverter.GetIntermediateValue(valueString, _jsonSerializer);
+ if (interValue is not DateTimeWithTimeZoneValueConverter.DateWithTimeZone dateWithTimeZone)
{
return null;
}
DateWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
- var valueObject = DateTimeWithTimeZoneValueConverter.GetValue(valueAsJsonObject, configuration);
- if (valueObject is DateTimeOffset && valueAsJsonObject["timeZone"] is null)
+ var objectValue = DateTimeWithTimeZoneValueConverter.GetObjectValue(dateWithTimeZone, configuration);
+
+ JsonNode node = new JsonObject();
+ if (objectValue is DateTimeOffset && dateWithTimeZone.TimeZone is null)
{
// If the time zone is not set, we assume the date is in UTC.
- valueAsJsonObject["timeZone"] = "UTC";
+ node["timeZone"] = "UTC";
}
- else if (valueObject is not null && valueObject is not DateTimeOffset)
+ else if (objectValue is not null && objectValue is not DateTimeOffset)
{
// If the value is not a DateTimeOffset, clean the time zone.
- valueAsJsonObject["timeZone"] = null;
+ node["timeZone"] = null;
+ }
+ else
+ {
+ node["timeZone"] = dateWithTimeZone.TimeZone;
}
- valueAsJsonObject["date"] = DateTimeWithTimeZoneValueConverter.GetDateValueAsString(valueObject, configuration);
+ node["date"] = DateTimeWithTimeZoneValueConverter.GetDateValueAsString(objectValue);
- return valueAsJsonObject;
+ return node;
}
///
@@ -130,7 +150,11 @@ private class DateTimeWithTimeZoneValidator : IValueValidator
public DateTimeWithTimeZoneValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
///
- public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
+ public IEnumerable Validate(
+ object? value,
+ string? valueType,
+ object? dataTypeConfiguration,
+ PropertyValidationContext validationContext)
{
if (value is not JsonObject valueAsJsonObject)
{
@@ -144,6 +168,21 @@ public IEnumerable Validate(object? value, string? valueType,
_localizedTextService.Localize("validation", "invalidDate", [selectedDate]),
["value"]);
}
+
+ var selectedTimeZone = valueAsJsonObject["timeZone"]?.GetValue();
+ var dataTypeConfig = dataTypeConfiguration as DateWithTimeZoneConfiguration;
+ if (dataTypeConfig?.TimeZones?.Mode is not { } mode || mode == DateWithTimeZoneMode.None)
+ {
+ yield break;
+ }
+
+ if (mode == DateWithTimeZoneMode.Custom
+ && dataTypeConfig.TimeZones.TimeZones.Any(t => t.Equals(selectedTimeZone, StringComparison.InvariantCultureIgnoreCase)) != true)
+ {
+ yield return new ValidationResult(
+ _localizedTextService.Localize("validation", "invalidTimeZone", [selectedTimeZone]),
+ ["value"]);
+ }
}
}
}
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
index fe9dc5923ff7..94c301c3112d 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
@@ -1,6 +1,7 @@
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
+using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -8,9 +9,15 @@ namespace Umbraco.Cms.Core.PropertyEditors;
public class DateTimeWithTimeZonePropertyIndexValueFactory : IDateTimeWithTimeZonePropertyIndexValueFactory
{
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
+ private readonly IJsonSerializer _jsonSerializer;
- public DateTimeWithTimeZonePropertyIndexValueFactory(IDataTypeConfigurationCache dataTypeConfigurationCache)
- => _dataTypeConfigurationCache = dataTypeConfigurationCache;
+ public DateTimeWithTimeZonePropertyIndexValueFactory(
+ IDataTypeConfigurationCache dataTypeConfigurationCache,
+ IJsonSerializer jsonSerializer)
+ {
+ _dataTypeConfigurationCache = dataTypeConfigurationCache;
+ _jsonSerializer = jsonSerializer;
+ }
///
public IEnumerable GetIndexValues(
@@ -36,7 +43,7 @@ public IEnumerable GetIndexValues(
}
DateWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
- var value = DateTimeWithTimeZoneValueConverter.GetValue(sourceStr, configuration);
+ var value = DateTimeWithTimeZoneValueConverter.GetValue(sourceStr, configuration, _jsonSerializer);
if (value is DateTimeOffset dateTimeOffset)
{
// Index the DateTimeOffset as UTC, so it's easier to query.
diff --git a/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfiguration.cs
index 50e3f6bdffe9..30589370c70e 100644
--- a/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfiguration.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfiguration.cs
@@ -13,18 +13,70 @@ public class DateWithTimeZoneConfiguration
[ConfigurationField("format")]
public DateWithTimeZoneFormat Format { get; set; } = DateWithTimeZoneFormat.DateTime;
+ ///
+ /// The time zones configuration.
+ ///
[ConfigurationField("timeZones")]
- public object? TimeZones { get; set; }
+ public DateWithTimeZoneTimeZones? TimeZones { get; set; }
+}
+
+public class DateWithTimeZoneTimeZones
+{
+ ///
+ /// The mode for time zones.
+ ///
+ public DateWithTimeZoneMode? Mode { get; set; } = DateWithTimeZoneMode.None;
+
+ ///
+ /// A list of time zones to use when the mode is set to Custom.
+ ///
+ public List TimeZones { get; set; } = [];
+}
+
+public enum DateWithTimeZoneMode
+{
+ ///
+ /// Do not display any time zones.
+ ///
+ [JsonStringEnumMemberName("none")]
+ None,
+
+ ///
+ /// Display all time zones.
+ ///
+ [JsonStringEnumMemberName("all")]
+ All,
+
+ ///
+ /// Display only the local time zone of the user.
+ ///
+ [JsonStringEnumMemberName("local")]
+ Local,
+
+ ///
+ /// Display a custom list of time zones defined in the configuration.
+ ///
+ [JsonStringEnumMemberName("custom")]
+ Custom,
}
public enum DateWithTimeZoneFormat
{
+ ///
+ /// Display the date only, without time.
+ ///
[JsonStringEnumMemberName("date-only")]
DateOnly,
+ ///
+ /// Display the time only, without date.
+ ///
[JsonStringEnumMemberName("time-only")]
TimeOnly,
+ ///
+ /// Display both date and time.
+ ///
[JsonStringEnumMemberName("date-time")]
DateTime,
}
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
index d7174dfc8046..09a990af2e3d 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
@@ -1,6 +1,6 @@
-using System.Globalization;
-using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
@@ -11,6 +11,15 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
[DefaultPropertyValueConverter]
public class DateTimeWithTimeZoneValueConverter : PropertyValueConverterBase
{
+ private readonly IJsonSerializer _jsonSerializer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The JSON serializer.
+ public DateTimeWithTimeZoneValueConverter(IJsonSerializer jsonSerializer)
+ => _jsonSerializer = jsonSerializer;
+
///
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTimeWithTimeZone);
@@ -33,11 +42,26 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
IPublishedPropertyType propertyType,
object? source,
bool preview)
+ {
+ var sourceStr = source?.ToString();
+ if (sourceStr is null || !_jsonSerializer.TryDeserialize(sourceStr, out DateWithTimeZone? dateWithTimeZone))
+ {
+ return null;
+ }
+
+ return dateWithTimeZone;
+ }
+
+ public override object? ConvertIntermediateToObject(
+ IPublishedElement owner,
+ IPublishedPropertyType propertyType,
+ PropertyCacheLevel referenceCacheLevel,
+ object? inter,
+ bool preview)
{
DateWithTimeZoneConfiguration? config =
ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
- var sourceStr = source?.ToString();
- return GetValue(sourceStr, config);
+ return GetObjectValue(inter, config);
}
private static Type GetPropertyValueType(DateWithTimeZoneConfiguration? config) =>
@@ -45,41 +69,58 @@ private static Type GetPropertyValueType(DateWithTimeZoneConfiguration? config)
{
DateWithTimeZoneFormat.DateOnly => typeof(DateOnly?),
DateWithTimeZoneFormat.TimeOnly => typeof(TimeOnly?),
- DateWithTimeZoneFormat.DateTime when config.TimeZones is null => typeof(DateTime?),
+ DateWithTimeZoneFormat.DateTime when config.TimeZones?.Mode is not { } mode || mode == DateWithTimeZoneMode.None => typeof(DateTime?),
_ => typeof(DateTimeOffset?),
};
- internal static object? GetValue(string? value, DateWithTimeZoneConfiguration? configuration)
- => GetValue(value is null ? null : JsonNode.Parse(value) as JsonObject, configuration);
+ internal static object? GetIntermediateValue(string? source, IJsonSerializer jsonSerializer)
+ {
+ if (source is null || !jsonSerializer.TryDeserialize(source, out DateWithTimeZone? dateWithTimeZone))
+ {
+ return null;
+ }
+
+ return dateWithTimeZone;
+ }
- internal static object? GetValue(JsonObject? value, DateWithTimeZoneConfiguration? configuration)
+ internal static object? GetObjectValue(object? inter, DateWithTimeZoneConfiguration? configuration)
{
Type propertyValueType = GetPropertyValueType(configuration);
- if (value is null
- || !DateTimeOffset.TryParse(value["date"]?.GetValue(), null, DateTimeStyles.AssumeUniversal, out DateTimeOffset dateTimeOffset))
+ if (inter is null)
+ {
+ return propertyValueType.GetDefaultValue();
+ }
+
+ if (inter is not DateWithTimeZone dateWithTimeZone)
{
return propertyValueType.GetDefaultValue();
}
if (propertyValueType == typeof(DateOnly?))
{
- return DateOnly.FromDateTime(dateTimeOffset.UtcDateTime);
+ return DateOnly.FromDateTime(dateWithTimeZone.Date.UtcDateTime);
}
if (propertyValueType == typeof(TimeOnly?))
{
- return TimeOnly.FromDateTime(dateTimeOffset.UtcDateTime);
+ return TimeOnly.FromDateTime(dateWithTimeZone.Date.UtcDateTime);
}
if (propertyValueType == typeof(DateTime?))
{
- return DateTime.SpecifyKind(dateTimeOffset.UtcDateTime, DateTimeKind.Unspecified);
+ return DateTime.SpecifyKind(dateWithTimeZone.Date.UtcDateTime, DateTimeKind.Unspecified);
}
- return dateTimeOffset;
+ return dateWithTimeZone.Date;
}
- internal static string? GetDateValueAsString(object? value, DateWithTimeZoneConfiguration? configuration) =>
+ internal static object? GetValue(string? source, DateWithTimeZoneConfiguration? configuration, IJsonSerializer jsonSerializer)
+ {
+ var intermediateValue = GetIntermediateValue(source, jsonSerializer);
+ return GetObjectValue(intermediateValue, configuration);
+ }
+
+ internal static string? GetDateValueAsString(object? value) =>
value switch
{
DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("o"),
@@ -89,4 +130,13 @@ private static Type GetPropertyValueType(DateWithTimeZoneConfiguration? config)
null => null,
_ => throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported type: {value.GetType().FullName}"),
};
+
+ public class DateWithTimeZone
+ {
+ [JsonPropertyName("date")]
+ public DateTimeOffset Date { get; init; }
+
+ [JsonPropertyName("timeZone")]
+ public string? TimeZone { get; init; }
+ }
}
From 26fb5ada299bbcb086a30cd240f81f75691b4b45 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 12 Aug 2025 15:05:11 +0200
Subject: [PATCH 25/86] Save examine value as ISO format
---
.../DateTimeWithTimeZonePropertyIndexValueFactory.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
index 94c301c3112d..904fb0436c0e 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
@@ -50,7 +50,7 @@ public IEnumerable GetIndexValues(
value = dateTimeOffset.UtcDateTime;
}
- indexValue.Values = [value];
+ indexValue.Values = [$"{value:O}"];
return [indexValue];
}
From d6b4c6c86b7e7e858d0ba2a2e99ed6751e032a9f Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 12 Aug 2025 16:34:18 +0200
Subject: [PATCH 26/86] Adjusting class names for consistency
---
...s => DateTimeWithTimeZoneConfiguration.cs} | 14 +++++-----
...DateTimeWithTimeZoneConfigurationEditor.cs | 14 ++++++++++
.../DateTimeWithTimeZonePropertyEditor.cs | 18 ++++++------
...meWithTimeZonePropertyIndexValueFactory.cs | 2 +-
.../DateWithTimeZoneConfigurationEditor.cs | 14 ----------
.../DateTimeWithTimeZoneValueConverter.cs | 28 +++++++++----------
6 files changed, 45 insertions(+), 45 deletions(-)
rename src/Umbraco.Core/PropertyEditors/{DateWithTimeZoneConfiguration.cs => DateTimeWithTimeZoneConfiguration.cs} (79%)
create mode 100644 src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfigurationEditor.cs
delete mode 100644 src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfigurationEditor.cs
diff --git a/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfiguration.cs
similarity index 79%
rename from src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfiguration.cs
rename to src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfiguration.cs
index 30589370c70e..2c1606bc3535 100644
--- a/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfiguration.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfiguration.cs
@@ -5,27 +5,27 @@
namespace Umbraco.Cms.Core.PropertyEditors;
-public class DateWithTimeZoneConfiguration
+public class DateTimeWithTimeZoneConfiguration
{
///
/// The format of the date and time value.
///
[ConfigurationField("format")]
- public DateWithTimeZoneFormat Format { get; set; } = DateWithTimeZoneFormat.DateTime;
+ public DateTimeWithTimeZoneFormat Format { get; set; } = DateTimeWithTimeZoneFormat.DateTime;
///
/// The time zones configuration.
///
[ConfigurationField("timeZones")]
- public DateWithTimeZoneTimeZones? TimeZones { get; set; }
+ public DateTimeWithTimeZoneTimeZones? TimeZones { get; set; }
}
-public class DateWithTimeZoneTimeZones
+public class DateTimeWithTimeZoneTimeZones
{
///
/// The mode for time zones.
///
- public DateWithTimeZoneMode? Mode { get; set; } = DateWithTimeZoneMode.None;
+ public DateTimeWithTimeZoneMode? Mode { get; set; } = DateTimeWithTimeZoneMode.None;
///
/// A list of time zones to use when the mode is set to Custom.
@@ -33,7 +33,7 @@ public class DateWithTimeZoneTimeZones
public List TimeZones { get; set; } = [];
}
-public enum DateWithTimeZoneMode
+public enum DateTimeWithTimeZoneMode
{
///
/// Do not display any time zones.
@@ -60,7 +60,7 @@ public enum DateWithTimeZoneMode
Custom,
}
-public enum DateWithTimeZoneFormat
+public enum DateTimeWithTimeZoneFormat
{
///
/// Display the date only, without time.
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfigurationEditor.cs
new file mode 100644
index 000000000000..0a863bfb5745
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfigurationEditor.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.IO;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
+
+internal class DateTimeWithTimeZoneConfigurationEditor : ConfigurationEditor
+{
+ public DateTimeWithTimeZoneConfigurationEditor(IIOHelper ioHelper)
+ : base(ioHelper)
+ {
+ }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
index 02baaef8985c..e67c3f0320a9 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
@@ -38,7 +38,7 @@ public DateTimeWithTimeZonePropertyEditor(
///
protected override IConfigurationEditor CreateConfigurationEditor() =>
- new DateWithTimeZoneConfigurationEditor(_ioHelper);
+ new DateTimeWithTimeZoneConfigurationEditor(_ioHelper);
///
protected override IDataValueEditor CreateValueEditor() =>
@@ -84,15 +84,15 @@ public DateTimeWithTimeZoneDataValueEditor(
return null;
}
- var configuration = editorValue.DataTypeConfiguration as DateWithTimeZoneConfiguration;
- if (configuration?.Format == DateWithTimeZoneFormat.TimeOnly)
+ var configuration = editorValue.DataTypeConfiguration as DateTimeWithTimeZoneConfiguration;
+ if (configuration?.Format == DateTimeWithTimeZoneFormat.TimeOnly)
{
// Clear the date part if the format is TimeOnly.
// This is needed because `DateTimeOffset.TryParse` does not support `DateTimeStyles.NoCurrentDateDefault`.
dateTimeOffset = new DateTimeOffset(DateOnly.MinValue, TimeOnly.FromTimeSpan(dateTimeOffset.TimeOfDay), dateTimeOffset.Offset);
}
- var value = new DateTimeWithTimeZoneValueConverter.DateWithTimeZone
+ var value = new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone
{
Date = dateTimeOffset,
TimeZone = valueAsJsonObject["timeZone"]?.GetValue(),
@@ -111,12 +111,12 @@ public DateTimeWithTimeZoneDataValueEditor(
}
var interValue = DateTimeWithTimeZoneValueConverter.GetIntermediateValue(valueString, _jsonSerializer);
- if (interValue is not DateTimeWithTimeZoneValueConverter.DateWithTimeZone dateWithTimeZone)
+ if (interValue is not DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone dateWithTimeZone)
{
return null;
}
- DateWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
+ DateTimeWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
var objectValue = DateTimeWithTimeZoneValueConverter.GetObjectValue(dateWithTimeZone, configuration);
JsonNode node = new JsonObject();
@@ -170,13 +170,13 @@ public IEnumerable Validate(
}
var selectedTimeZone = valueAsJsonObject["timeZone"]?.GetValue();
- var dataTypeConfig = dataTypeConfiguration as DateWithTimeZoneConfiguration;
- if (dataTypeConfig?.TimeZones?.Mode is not { } mode || mode == DateWithTimeZoneMode.None)
+ var dataTypeConfig = dataTypeConfiguration as DateTimeWithTimeZoneConfiguration;
+ if (dataTypeConfig?.TimeZones?.Mode is not { } mode || mode == DateTimeWithTimeZoneMode.None)
{
yield break;
}
- if (mode == DateWithTimeZoneMode.Custom
+ if (mode == DateTimeWithTimeZoneMode.Custom
&& dataTypeConfig.TimeZones.TimeZones.Any(t => t.Equals(selectedTimeZone, StringComparison.InvariantCultureIgnoreCase)) != true)
{
yield return new ValidationResult(
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
index 904fb0436c0e..c5c24193d195 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
@@ -42,7 +42,7 @@ public IEnumerable GetIndexValues(
return [indexValue];
}
- DateWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
+ DateTimeWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
var value = DateTimeWithTimeZoneValueConverter.GetValue(sourceStr, configuration, _jsonSerializer);
if (value is DateTimeOffset dateTimeOffset)
{
diff --git a/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfigurationEditor.cs
deleted file mode 100644
index 09a3554f6fd7..000000000000
--- a/src/Umbraco.Core/PropertyEditors/DateWithTimeZoneConfigurationEditor.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Umbraco.
-// See LICENSE for more details.
-
-using Umbraco.Cms.Core.IO;
-
-namespace Umbraco.Cms.Core.PropertyEditors;
-
-internal class DateWithTimeZoneConfigurationEditor : ConfigurationEditor
-{
- public DateWithTimeZoneConfigurationEditor(IIOHelper ioHelper)
- : base(ioHelper)
- {
- }
-}
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
index 09a990af2e3d..46a8dce7932d 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
@@ -27,8 +27,8 @@ public override bool IsConverter(IPublishedPropertyType propertyType)
///
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
{
- DateWithTimeZoneConfiguration? config =
- ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
+ DateTimeWithTimeZoneConfiguration? config =
+ ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
return GetPropertyValueType(config);
}
@@ -44,7 +44,7 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
bool preview)
{
var sourceStr = source?.ToString();
- if (sourceStr is null || !_jsonSerializer.TryDeserialize(sourceStr, out DateWithTimeZone? dateWithTimeZone))
+ if (sourceStr is null || !_jsonSerializer.TryDeserialize(sourceStr, out DateTimeWithTimeZone? dateWithTimeZone))
{
return null;
}
@@ -59,23 +59,23 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
object? inter,
bool preview)
{
- DateWithTimeZoneConfiguration? config =
- ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
+ DateTimeWithTimeZoneConfiguration? config =
+ ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
return GetObjectValue(inter, config);
}
- private static Type GetPropertyValueType(DateWithTimeZoneConfiguration? config) =>
+ private static Type GetPropertyValueType(DateTimeWithTimeZoneConfiguration? config) =>
config?.Format switch
{
- DateWithTimeZoneFormat.DateOnly => typeof(DateOnly?),
- DateWithTimeZoneFormat.TimeOnly => typeof(TimeOnly?),
- DateWithTimeZoneFormat.DateTime when config.TimeZones?.Mode is not { } mode || mode == DateWithTimeZoneMode.None => typeof(DateTime?),
+ DateTimeWithTimeZoneFormat.DateOnly => typeof(DateOnly?),
+ DateTimeWithTimeZoneFormat.TimeOnly => typeof(TimeOnly?),
+ DateTimeWithTimeZoneFormat.DateTime when config.TimeZones?.Mode is not { } mode || mode == DateTimeWithTimeZoneMode.None => typeof(DateTime?),
_ => typeof(DateTimeOffset?),
};
internal static object? GetIntermediateValue(string? source, IJsonSerializer jsonSerializer)
{
- if (source is null || !jsonSerializer.TryDeserialize(source, out DateWithTimeZone? dateWithTimeZone))
+ if (source is null || !jsonSerializer.TryDeserialize(source, out DateTimeWithTimeZone? dateWithTimeZone))
{
return null;
}
@@ -83,7 +83,7 @@ private static Type GetPropertyValueType(DateWithTimeZoneConfiguration? config)
return dateWithTimeZone;
}
- internal static object? GetObjectValue(object? inter, DateWithTimeZoneConfiguration? configuration)
+ internal static object? GetObjectValue(object? inter, DateTimeWithTimeZoneConfiguration? configuration)
{
Type propertyValueType = GetPropertyValueType(configuration);
if (inter is null)
@@ -91,7 +91,7 @@ private static Type GetPropertyValueType(DateWithTimeZoneConfiguration? config)
return propertyValueType.GetDefaultValue();
}
- if (inter is not DateWithTimeZone dateWithTimeZone)
+ if (inter is not DateTimeWithTimeZone dateWithTimeZone)
{
return propertyValueType.GetDefaultValue();
}
@@ -114,7 +114,7 @@ private static Type GetPropertyValueType(DateWithTimeZoneConfiguration? config)
return dateWithTimeZone.Date;
}
- internal static object? GetValue(string? source, DateWithTimeZoneConfiguration? configuration, IJsonSerializer jsonSerializer)
+ internal static object? GetValue(string? source, DateTimeWithTimeZoneConfiguration? configuration, IJsonSerializer jsonSerializer)
{
var intermediateValue = GetIntermediateValue(source, jsonSerializer);
return GetObjectValue(intermediateValue, configuration);
@@ -131,7 +131,7 @@ private static Type GetPropertyValueType(DateWithTimeZoneConfiguration? config)
_ => throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported type: {value.GetType().FullName}"),
};
- public class DateWithTimeZone
+ public class DateTimeWithTimeZone
{
[JsonPropertyName("date")]
public DateTimeOffset Date { get; init; }
From 03f250cd19df2dcf352df5298a9ef4b02184ce59 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 12 Aug 2025 17:39:49 +0200
Subject: [PATCH 27/86] Small fixes
---
src/Umbraco.Core/EmbeddedResources/Lang/en.xml | 1 +
.../PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs | 2 +-
.../ValueConverters/DateTimeWithTimeZoneValueConverter.cs | 5 +----
3 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
index a45789182a2e..0732bfe77195 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
@@ -376,6 +376,7 @@
Value cannot be null
Value cannot be empty
Value is invalid, it does not match the correct pattern
+ Invalid date
%1% more.]]>
%1% too many.]]>
The string length exceeds the maximum length of %0% characters, %1% too many.
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
index e67c3f0320a9..35813874633f 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
@@ -180,7 +180,7 @@ public IEnumerable Validate(
&& dataTypeConfig.TimeZones.TimeZones.Any(t => t.Equals(selectedTimeZone, StringComparison.InvariantCultureIgnoreCase)) != true)
{
yield return new ValidationResult(
- _localizedTextService.Localize("validation", "invalidTimeZone", [selectedTimeZone]),
+ _localizedTextService.Localize("validation", "notOneOfOptions", [selectedTimeZone]),
["value"]);
}
}
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
index 46a8dce7932d..f962a6641f42 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
@@ -123,10 +123,7 @@ private static Type GetPropertyValueType(DateTimeWithTimeZoneConfiguration? conf
internal static string? GetDateValueAsString(object? value) =>
value switch
{
- DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("o"),
- DateOnly dateOnly => dateOnly.ToString("o"),
- TimeOnly timeOnly => timeOnly.ToString("o"),
- DateTime dateTime => dateTime.ToString("o"),
+ DateTimeOffset or DateOnly or TimeOnly or DateTime => $"{value:O}",
null => null,
_ => throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported type: {value.GetType().FullName}"),
};
From 916b2dcea19b0f2191533587c29f239d2eee2f79 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 12 Aug 2025 17:42:00 +0200
Subject: [PATCH 28/86] Add default data type configuration
---
src/Umbraco.Core/Constants-DataTypes.cs | 20 ++++++-------
.../Migrations/Install/DatabaseDataCreator.cs | 29 +++++++++++++++----
2 files changed, 34 insertions(+), 15 deletions(-)
diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs
index f013fb064fdd..ffcfef2c3795 100644
--- a/src/Umbraco.Core/Constants-DataTypes.cs
+++ b/src/Umbraco.Core/Constants-DataTypes.cs
@@ -115,6 +115,11 @@ public static class Guids
///
public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a";
+ ///
+ /// Guid for Date Time With Timezone as string
+ ///
+ public const string DateTimeWithTimeZone = "88E8A052-30EE-4D44-A507-59F2CDFC769C";
+
///
/// Guid for Approved Color as string
///
@@ -225,11 +230,6 @@ public static class Guids
///
public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64";
- ///
- /// Guid for Date Picker with timezone
- ///
- public const string DatePickerWithTimezone = "e7ccd055-60e2-4c63-88e7-212249eaf581";
-
///
/// Guid for Content Picker
///
@@ -295,6 +295,11 @@ public static class Guids
///
public static readonly Guid DatePickerWithTimeGuid = new(DatePickerWithTime);
+ ///
+ /// Guid for for Date Time With Timezone
+ ///
+ public static readonly Guid DateTimeWithTimeZoneGuid = new(DateTimeWithTimeZone);
+
///
/// Guid for Approved Color
///
@@ -404,11 +409,6 @@ public static class Guids
/// Guid for Label decimal
///
public static readonly Guid LabelDecimalGuid = new(LabelDecimal);
-
- ///
- /// Guid for Date Picker with timezone
- ///
- public static readonly Guid DatePickerWithTimezoneGuid = new(DatePickerWithTimezone);
}
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index 0613f1f20918..f2e749c97755 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -857,6 +857,25 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text)
},
Constants.DatabaseSchema.Tables.Node,
"id");
+ ConditionalInsert(
+ Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes,
+ Constants.DataTypes.Guids.DateTimeWithTimeZone,
+ new NodeDto
+ {
+ NodeId = 1055,
+ Trashed = false,
+ ParentId = -1,
+ UserId = -1,
+ Level = 1,
+ Path = "-1,1055",
+ SortOrder = 2,
+ UniqueId = Constants.DataTypes.Guids.DateTimeWithTimeZoneGuid,
+ Text = "Date Picker with Time Zone",
+ NodeObjectType = Constants.ObjectTypes.DataType,
+ CreateDate = DateTime.Now,
+ },
+ Constants.DatabaseSchema.Tables.Node,
+ "id");
}
private void CreateNodeDataForMediaTypes()
@@ -2293,7 +2312,7 @@ string GridCollectionView(string collectionViewType) =>
});
}
- /*if (_database.Exists(1055))
+ if (_database.Exists(1055))
{
_database.Insert(
Constants.DatabaseSchema.Tables.DataType,
@@ -2302,12 +2321,12 @@ string GridCollectionView(string collectionViewType) =>
new DataTypeDto
{
NodeId = 1055,
- EditorAlias = Constants.PropertyEditors.Aliases.DateTimeWithTimezone,
- EditorUiAlias = "Umb.PropertyEditorUi.DatePicker",
+ EditorAlias = Constants.PropertyEditors.Aliases.DateTimeWithTimeZone,
+ EditorUiAlias = "Umb.PropertyEditorUi.DateWithTimeZonePicker",
DbType = "Ntext",
- Configuration = "{\"timezones\": [\"UTC\"], \"format\": \"YYYY-MM-DD HH:mm:ss\"}",
+ Configuration = "{\"format\": \"date-time\", \"timeFormat\": \"HH:mm\", \"timeZones\": {\"mode\": \"all\"}}",
});
- }*/
+ }
}
private void CreateRelationTypeData()
From b7d399a445c1706e87d35a0b6271f4d268fa84ff Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Thu, 14 Aug 2025 14:58:28 +0200
Subject: [PATCH 29/86] Rename `TimeZone` to `UmbTimeZone`
---
.../input-time-zone/input-time-zone-picker.element.ts | 6 +++---
.../input-time-zone/input-time-zone.element.ts | 4 ++--
.../src/packages/core/utils/date/date.timezone.ts | 10 +++++-----
...rty-editor-ui-date-with-time-zone-picker.element.ts | 8 ++++----
4 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
index 27e9ddf1cc2a..e2e941402ca8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
@@ -1,7 +1,7 @@
import { html, customElement, css, property, query, until, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
-import type { TimeZone } from '@umbraco-cms/backoffice/utils';
+import type { UmbTimeZone } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
/**
@@ -35,7 +35,7 @@ export class UmbInputTimeZonePickerElement extends UmbLitElement {
public get options() {
return this.#options;
}
- #options: Array = [];
+ #options: Array = [];
@property({ type: String, reflect: true })
public set value(value) {
@@ -47,7 +47,7 @@ export class UmbInputTimeZonePickerElement extends UmbLitElement {
#value: string = '';
@state()
- private _filteredOptions: Array = [];
+ private _filteredOptions: Array = [];
@query('#input')
protected _input?: UUIComboboxElement;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
index 5db2f80efb41..7116c199ecc3 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
@@ -15,7 +15,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import { getTimeZoneList, type TimeZone } from '@umbraco-cms/backoffice/utils';
+import { getTimeZoneList, type UmbTimeZone } from '@umbraco-cms/backoffice/utils';
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
/**
@@ -126,7 +126,7 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
@query('umb-input-time-zone-picker')
protected _timeZonePicker?: UmbInputTimeZonePickerElement;
- private _timeZoneList: Array = [];
+ private _timeZoneList: Array = [];
constructor() {
super();
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/date/date.timezone.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/date/date.timezone.ts
index ab64488233c5..3a6b6b9dc27b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/date/date.timezone.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/date/date.timezone.ts
@@ -1,6 +1,6 @@
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
-export interface TimeZone {
+export interface UmbTimeZone {
value: string;
name: string;
offset: string;
@@ -10,12 +10,12 @@ export interface TimeZone {
* Retrieves a list of supported time zones in the browser.
* @param {Array} [filter] - An optional array of time zone identifiers to filter the result on.
* @param {DateTime} [selectedDate] - An optional Luxon DateTime object to format the offset for each time zone.
- * @returns {Array} An array of objects containing time zone values and names.
+ * @returns {Array} An array of objects containing time zone values and names.
*/
export function getTimeZoneList(
filter: Array | undefined = undefined,
selectedDate: DateTime | undefined = undefined,
-): Array {
+): Array {
if (filter) {
return filter.map((tz) => ({
value: tz,
@@ -42,9 +42,9 @@ export function getTimeZoneList(
/**
* Retrieves the client's time zone information.
* @param {DateTime} [selectedDate] - An optional Luxon DateTime object to format the offset of the time zone.
- * @returns {TimeZone} An object containing the client's time zone name and value.
+ * @returns {UmbTimeZone} An object containing the client's time zone name and value.
*/
-export function getClientTimeZone(selectedDate: DateTime | undefined = undefined): TimeZone {
+export function getClientTimeZone(selectedDate: DateTime | undefined = undefined): UmbTimeZone {
const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return {
value: clientTimeZone,
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
index 71e65643eb85..a283c97468f8 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
@@ -1,4 +1,4 @@
-import { getClientTimeZone, getTimeZoneList, isEquivalentTimeZone, type TimeZone } from '@umbraco-cms/backoffice/utils';
+import { getClientTimeZone, getTimeZoneList, isEquivalentTimeZone, type UmbTimeZone } from '@umbraco-cms/backoffice/utils';
import type {
UmbPropertyEditorConfigCollection,
UmbPropertyEditorUiElement,
@@ -28,8 +28,8 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
- private _timeZoneOptions: Array = [];
- private _clientTimeZone: TimeZone | undefined;
+ private _timeZoneOptions: Array = [];
+ private _clientTimeZone: UmbTimeZone | undefined;
@state()
private _value: UmbDateTimeWithTimeZone | undefined;
@@ -62,7 +62,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
private _datePickerValue: string = '';
@state()
- private _filteredTimeZoneOptions: Array = [];
+ private _filteredTimeZoneOptions: Array = [];
@state()
private _selectedTimeZone: string | undefined;
From ad37544bd26fade3af79e1f7c54f5477b25c6845 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Thu, 14 Aug 2025 15:07:35 +0200
Subject: [PATCH 30/86] Fix failing tests
---
.../Repositories/DataTypeDefinitionRepositoryTest.cs | 4 ++--
.../Umbraco.Core/Composing/TypeLoaderTests.cs | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
index 66ac4278a3ca..f795e80cd29d 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
@@ -250,7 +250,7 @@ public void Can_Perform_GetAll_On_DataTypeDefinitionRepository()
Assert.That(dataTypeDefinitions, Is.Not.Null);
Assert.That(dataTypeDefinitions.Any(), Is.True);
Assert.That(dataTypeDefinitions.Any(x => x == null), Is.False);
- Assert.That(dataTypeDefinitions.Length, Is.EqualTo(34));
+ Assert.That(dataTypeDefinitions.Length, Is.EqualTo(35));
}
}
@@ -297,7 +297,7 @@ public void Can_Perform_Count_On_DataTypeDefinitionRepository()
var count = DataTypeRepository.Count(query);
// Assert
- Assert.That(count, Is.EqualTo(4));
+ Assert.That(count, Is.EqualTo(5));
}
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs
index 068bf44db66f..4987432aea70 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs
@@ -132,7 +132,7 @@ public void Resolves_Types()
public void GetDataEditors()
{
var types = _typeLoader.GetDataEditors();
- Assert.AreEqual(36, types.Count());
+ Assert.AreEqual(37, types.Count());
}
///
From 5d891db723f12631324c6e7ff711a3e97ebc6f2c Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Wed, 20 Aug 2025 15:35:30 +0200
Subject: [PATCH 31/86] Started adding unit tests for
DateWithTimeZonePropertyEditor
---
.../DateTimeWithTimeZonePropertyEditor.cs | 2 +-
.../DateWithTimeZonePropertyEditorTests.cs | 167 ++++++++++++++++++
2 files changed, 168 insertions(+), 1 deletion(-)
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateWithTimeZonePropertyEditorTests.cs
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
index 35813874633f..417fae02e58f 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
@@ -47,7 +47,7 @@ protected override IDataValueEditor CreateValueEditor() =>
///
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _propertyIndexValueFactory;
- private class DateTimeWithTimeZoneDataValueEditor : DataValueEditor
+ internal class DateTimeWithTimeZoneDataValueEditor : DataValueEditor
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateWithTimeZonePropertyEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateWithTimeZonePropertyEditorTests.cs
new file mode 100644
index 000000000000..34563ec45848
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateWithTimeZonePropertyEditorTests.cs
@@ -0,0 +1,167 @@
+using System.Globalization;
+using System.Text.Json.Nodes;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.Models.Validation;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Strings;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
+
+[TestFixture]
+public class DateWithTimeZonePropertyEditorTests
+{
+ private readonly Mock _jsonSerializerMock = new(MockBehavior.Strict);
+
+ private static readonly object[] _sourceList1 =
+ [
+ new object[] { null, true },
+ new object[] { "INVALID", false },
+ new object[] { JsonNode.Parse("{}"), false },
+ new object[] { JsonNode.Parse("{\"test\": \"\"}"), false },
+ new object[] { JsonNode.Parse("{\"date\": \"\"}"), false },
+ new object[] { JsonNode.Parse("{\"date\": \"INVALID\"}"), false },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), true }
+ ];
+
+ [TestCaseSource(nameof(_sourceList1))]
+ public void Validates_Date_Received(object? value, bool expectedSuccess)
+ {
+ var editor = CreateValueEditor();
+ var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()).ToList();
+ if (expectedSuccess)
+ {
+ Assert.IsEmpty(result);
+ }
+ else
+ {
+ Assert.AreEqual(1, result.Count);
+
+ var validationResult = result.First();
+ Assert.AreEqual("validation_invalidDate", validationResult.ErrorMessage);
+ }
+ }
+
+ private static readonly object[] _sourceList2 =
+ [
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTimeWithTimeZoneMode.None, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTimeWithTimeZoneMode.All, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTimeWithTimeZoneMode.Local, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTimeWithTimeZoneMode.Custom, new[] { "Europe/Copenhagen" }, false },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.None, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.All, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Local, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Custom, Array.Empty(), false },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Custom, new[] { "Europe/Copenhagen" }, true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Custom, new[] { "Europe/Amsterdam", "Europe/Copenhagen" }, true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Custom, new[] { "Europe/Amsterdam" }, false },
+ ];
+
+ [TestCaseSource(nameof(_sourceList2))]
+ public void Validates_TimeZone_Received(
+ object value,
+ DateTimeWithTimeZoneMode timeZoneMode,
+ string[] timeZones,
+ bool expectedSuccess)
+ {
+ var editor = CreateValueEditor(timeZoneMode: timeZoneMode, timeZones: timeZones);
+ var result = editor.Validate(value, false, null, PropertyValidationContext.Empty()).ToList();
+ if (expectedSuccess)
+ {
+ Assert.IsEmpty(result);
+ }
+ else
+ {
+ Assert.AreEqual(1, result.Count);
+
+ var validationResult = result.First();
+ Assert.AreEqual("validation_notOneOfOptions", validationResult.ErrorMessage);
+ }
+ }
+
+ private static readonly object[] _sourceList3 =
+ [
+ new object[] { null, DateTimeWithTimeZoneFormat.DateOnly, null, null },
+ new object[] { JsonNode.Parse("{}"), DateTimeWithTimeZoneFormat.DateTime, null, null },
+ new object[] { JsonNode.Parse("{\"INVALID\": \"\"}"), DateTimeWithTimeZoneFormat.DateTime, null, null },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20\"}"), DateTimeWithTimeZoneFormat.DateOnly, new DateTimeOffset(2025, 8, 20, 0, 0, 0, TimeSpan.Zero), null },
+ new object[] { JsonNode.Parse("{\"date\": \"16:34\"}"), DateTimeWithTimeZoneFormat.TimeOnly, new DateTimeOffset(1, 1, 1, 16, 34, 0, TimeSpan.Zero), null },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01\"}"), DateTimeWithTimeZoneFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.Zero), null },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01Z\"}"), DateTimeWithTimeZoneFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.Zero), null },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01-05:00\"}"), DateTimeWithTimeZoneFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.FromHours(-5)), null },
+ ];
+
+ [TestCaseSource(nameof(_sourceList3))]
+ public void Can_Parse_Values_From_Editor(
+ object? value,
+ DateTimeWithTimeZoneFormat format,
+ DateTimeOffset? expectedDateTimeOffset,
+ string? expectedTimeZone)
+ {
+ var expectedJson = expectedDateTimeOffset is null ? null : "RESULT_JSON";
+ _jsonSerializerMock.Setup(s => s.Serialize(It.IsAny()))
+ .Returns((object _) => expectedJson)
+ .Callback
- ${this.#renderTimeZoneInfo()}
+ ${this.#renderTimeZoneInfo()} ${this.#renderTimeZoneValidation()}
`;
}
@@ -368,9 +368,11 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
`;
}
- #renderTimeZoneOption = (option: Option) =>
- html`
- ${option.name}
+ #renderTimeZoneOption = (option: UmbTimeZoneOption) =>
+ html`
+ ${option.name + (option.invalid ? ` (${this.localize.term('validation_legacyOption')})` : '')}
`;
#renderTimeZoneInfo() {
@@ -392,6 +394,18 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
>`;
}
+ #renderTimeZoneValidation() {
+ const selectionHasInvalids = this._timeZoneOptions.some((option) => {
+ return option.invalid && option.value === this._selectedTimeZone;
+ });
+
+ if (selectionHasInvalids) {
+ return html`
`;
+ }
+
+ return nothing;
+ }
+
static override readonly styles = [
css`
:host {
@@ -408,6 +422,10 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
font-size: var(--uui-type-small-size);
font-weight: normal;
}
+ .error {
+ color: var(--uui-color-invalid);
+ font-size: var(--uui-font-size-small);
+ }
`,
];
}
From a458efd877a5431d664e7afaf880aa7f797f4978 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Thu, 21 Aug 2025 12:38:29 +0200
Subject: [PATCH 38/86] Do not preselect a time zone if a date is stored
without time zone
This most likely means that the configuration of the editor changed to add time zone support. In this case we want to force the editor to select the applicable time zone.
---
.../DateTimeWithTimeZonePropertyEditor.cs | 16 +---------------
...itor-ui-date-with-time-zone-picker.element.ts | 2 ++
2 files changed, 3 insertions(+), 15 deletions(-)
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
index 37883e34b8e5..f5266424396b 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
@@ -121,21 +121,7 @@ public DateTimeWithTimeZoneDataValueEditor(
JsonNode node = new JsonObject();
node["date"] = DateTimeWithTimeZoneValueConverter.GetDateValueAsString(objectValue);
-
- if (objectValue is DateTimeOffset && dateWithTimeZone.TimeZone is null)
- {
- // If the time zone is not set, we assume the date is in UTC.
- node["timeZone"] = "UTC";
- }
- else if (objectValue is not null && objectValue is not DateTimeOffset)
- {
- // If the value is not a DateTimeOffset, clean the time zone.
- node["timeZone"] = null;
- }
- else
- {
- node["timeZone"] = dateWithTimeZone.TimeZone;
- }
+ node["timeZone"] = dateWithTimeZone.TimeZone;
return node;
}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
index 2ec3fcd21500..65a94cdb760b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
@@ -197,6 +197,8 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
this._selectedTimeZone = pickedTimeZone.value;
return;
}
+ } else if (this.value?.date) {
+ return; // If there is a date but no time zone, we don't preselect anything
}
// Check if we can pre-select the client time zone
From e2a70184d42a32f9d7a8a45dac87cc122798a9ec Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Thu, 21 Aug 2025 14:56:05 +0200
Subject: [PATCH 39/86] Fix failing backoffice build
---
.../input-time-zone-picker.element.ts | 6 +++++-
.../input-time-zone.element.ts | 12 ++++++++----
...r-ui-date-with-time-zone-picker.element.ts | 19 ++++++++++---------
3 files changed, 23 insertions(+), 14 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
index e2e941402ca8..5a0c4b9d9410 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone-picker.element.ts
@@ -4,6 +4,10 @@ import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffi
import type { UmbTimeZone } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
+export interface UmbTimeZoneOption extends UmbTimeZone {
+ offset: string;
+}
+
/**
* @element umb-input-time-zone-picker
*/
@@ -35,7 +39,7 @@ export class UmbInputTimeZonePickerElement extends UmbLitElement {
public get options() {
return this.#options;
}
- #options: Array = [];
+ #options: Array = [];
@property({ type: String, reflect: true })
public set value(value) {
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
index 7116c199ecc3..d406d4b6e2ac 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-time-zone/input-time-zone.element.ts
@@ -1,5 +1,5 @@
import type UmbInputTimeZoneItemElement from './input-time-zone-item.element.js';
-import type { UmbInputTimeZonePickerElement } from './input-time-zone-picker.element.js';
+import type { UmbInputTimeZonePickerElement, UmbTimeZoneOption } from './input-time-zone-picker.element.js';
import {
html,
customElement,
@@ -15,7 +15,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
-import { getTimeZoneList, type UmbTimeZone } from '@umbraco-cms/backoffice/utils';
+import { getTimeZoneList, getTimeZoneOffset } from '@umbraco-cms/backoffice/utils';
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
/**
@@ -126,11 +126,15 @@ export class UmbInputTimeZoneElement extends UmbFormControlMixin,
@query('umb-input-time-zone-picker')
protected _timeZonePicker?: UmbInputTimeZonePickerElement;
- private _timeZoneList: Array = [];
+ private _timeZoneList: Array = [];
constructor() {
super();
- this._timeZoneList = getTimeZoneList(undefined, DateTime.now());
+ const now = DateTime.now();
+ this._timeZoneList = getTimeZoneList(undefined).map((tz) => ({
+ ...tz,
+ offset: getTimeZoneOffset(tz.value, now), // Format offset as string
+ }));
this.addValidator(
'rangeUnderflow',
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
index 65a94cdb760b..2beb4e5143d7 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
@@ -26,7 +26,7 @@ import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffi
import type { UmbDateTimeWithTimeZone, UmbTimeZonePickerValue } from '@umbraco-cms/backoffice/models';
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
-interface UmbTimeZoneOption extends UmbTimeZone {
+interface UmbTimeZonePickerOption extends UmbTimeZone {
offset: string;
invalid: boolean;
}
@@ -38,7 +38,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
{
- private _timeZoneOptions: Array = [];
+ private _timeZoneOptions: Array = [];
private _clientTimeZone: UmbTimeZone | undefined;
@state()
@@ -72,7 +72,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
private _datePickerValue: string = '';
@state()
- private _filteredTimeZoneOptions: Array = [];
+ private _filteredTimeZoneOptions: Array = [];
@state()
private _selectedTimeZone: string | undefined;
@@ -140,18 +140,19 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
this._clientTimeZone = getClientTimeZone();
// Retrieve the time zones from the config
+ const dateToCalculateOffset = selectedDate ?? DateTime.now();
switch (timeZonePickerConfig?.mode) {
case 'all':
this._timeZoneOptions = this._filteredTimeZoneOptions = getTimeZoneList(undefined).map((tz) => ({
...tz,
- offset: getTimeZoneOffset(tz.value, selectedDate ?? DateTime.now()),
+ offset: getTimeZoneOffset(tz.value, dateToCalculateOffset),
invalid: false,
}));
break;
case 'local': {
this._timeZoneOptions = this._filteredTimeZoneOptions = [this._clientTimeZone].map((tz) => ({
...tz,
- offset: getTimeZoneOffset(tz.value, selectedDate ?? DateTime.now()),
+ offset: getTimeZoneOffset(tz.value, dateToCalculateOffset),
invalid: false,
}));
break;
@@ -159,7 +160,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
case 'custom': {
this._timeZoneOptions = this._filteredTimeZoneOptions = getTimeZoneList(timeZonePickerConfig.timeZones).map((tz) => ({
...tz,
- offset: getTimeZoneOffset(tz.value, selectedDate ?? DateTime.now()),
+ offset: getTimeZoneOffset(tz.value, dateToCalculateOffset),
invalid: false,
}),
);
@@ -168,10 +169,10 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
selectedTimeZone &&
!this._timeZoneOptions.some((opt) => isEquivalentTimeZone(opt.value, selectedTimeZone))) {
// If the selected time zone is not in the list, we add it to the options
- const customTimeZone: UmbTimeZoneOption = {
+ const customTimeZone: UmbTimeZonePickerOption = {
value: selectedTimeZone,
name: selectedTimeZone,
- offset: getTimeZoneOffset(selectedTimeZone, selectedDate ?? DateTime.now()),
+ offset: getTimeZoneOffset(selectedTimeZone, dateToCalculateOffset),
invalid: true, // Mark as invalid, as it is not in the list of supported time zones
};
this._timeZoneOptions.push(customTimeZone);
@@ -370,7 +371,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
`;
}
- #renderTimeZoneOption = (option: UmbTimeZoneOption) =>
+ #renderTimeZoneOption = (option: UmbTimeZonePickerOption) =>
html`
From 239011505c5d1202d2c8e36782b0e2eef14a3510 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Thu, 21 Aug 2025 15:38:05 +0200
Subject: [PATCH 40/86] Added tests for
DateTimeWithTimeZonePropertyIndexValueFactory
---
...meWithTimeZonePropertyIndexValueFactory.cs | 5 ++
...thTimeZonePropertyIndexValueFactoryTest.cs | 86 +++++++++++++++++++
2 files changed, 91 insertions(+)
create mode 100644 tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactoryTest.cs
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
index c5c24193d195..9abfb7859561 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
@@ -44,6 +44,11 @@ public IEnumerable GetIndexValues(
DateTimeWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
var value = DateTimeWithTimeZoneValueConverter.GetValue(sourceStr, configuration, _jsonSerializer);
+ if (value is null)
+ {
+ return [indexValue];
+ }
+
if (value is DateTimeOffset dateTimeOffset)
{
// Index the DateTimeOffset as UTC, so it's easier to query.
diff --git a/tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactoryTest.cs b/tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactoryTest.cs
new file mode 100644
index 000000000000..1c07df455ff2
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactoryTest.cs
@@ -0,0 +1,86 @@
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Infrastructure.Serialization;
+
+namespace Umbraco.Cms.Tests.UnitTests.PropertyEditors;
+
+[TestFixture]
+[TestOf(typeof(DateTimeWithTimeZonePropertyIndexValueFactory))]
+public class DateTimeWithTimeZonePropertyIndexValueFactoryTest
+{
+ private readonly IJsonSerializer _jsonSerializer =
+ new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory());
+
+ private readonly Mock _dataTypeConfigurationCache = new(MockBehavior.Strict);
+
+ [Test]
+ public void GetIndexValues_ReturnsEmptyValues_ForNullPropertyValue()
+ {
+ var propertyMock = new Mock(MockBehavior.Strict);
+ propertyMock.SetupGet(x => x.Alias)
+ .Returns("testAlias");
+ propertyMock.Setup(x => x.GetValue("en-US", null, true))
+ .Returns(null);
+ var factory = new DateTimeWithTimeZonePropertyIndexValueFactory(_dataTypeConfigurationCache.Object, _jsonSerializer);
+
+ var result = factory.GetIndexValues(
+ propertyMock.Object,
+ "en-US",
+ null,
+ true,
+ [],
+ new Dictionary())
+ .ToList();
+
+ Assert.AreEqual(1, result.Count);
+ var indexValue = result.First();
+ Assert.AreEqual(indexValue.FieldName, "testAlias");
+ Assert.IsEmpty(indexValue.Values);
+ }
+
+ [TestCase(DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.All, "{\"date\":\"2023-01-18T12:00:00+01:00\",\"timeZone\":\"Europe/Copenhagen\"}", "2023-01-18T11:00:00.0000000Z")]
+ [TestCase(DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.None, "{\"date\":\"2023-01-18T12:00:00Z\",\"timeZone\":\"Europe/Copenhagen\"}", "2023-01-18T12:00:00.0000000")]
+ [TestCase(DateTimeWithTimeZoneFormat.DateOnly, DateTimeWithTimeZoneMode.None, "{\"date\":\"2023-01-18T00:00:00Z\",\"timeZone\":null}", "2023-01-18")]
+ [TestCase(DateTimeWithTimeZoneFormat.TimeOnly, DateTimeWithTimeZoneMode.None, "{\"date\":\"0001-01-01T12:00:00Z\",\"timeZone\":null}", "12:00:00.0000000")]
+ public void GetIndexValues_ReturnsFormattedUtcDateTime(DateTimeWithTimeZoneFormat format, DateTimeWithTimeZoneMode mode, string propertyValue, string expectedIndexValue)
+ {
+ var dataTypeKey = Guid.NewGuid();
+ var propertyMock = new Mock(MockBehavior.Strict);
+ propertyMock.SetupGet(x => x.Alias)
+ .Returns("testAlias");
+ propertyMock.SetupGet(x => x.PropertyType)
+ .Returns(Mock.Of(x => x.DataTypeKey == dataTypeKey));
+ propertyMock.Setup(x => x.GetValue("en-US", null, true))
+ .Returns(propertyValue);
+
+ var configuration = new DateTimeWithTimeZoneConfiguration
+ {
+ Format = format,
+ TimeZones = new DateTimeWithTimeZoneTimeZones { Mode = mode },
+ };
+ _dataTypeConfigurationCache.Setup(x => x.GetConfigurationAs(dataTypeKey))
+ .Returns(configuration);
+
+ var factory = new DateTimeWithTimeZonePropertyIndexValueFactory(_dataTypeConfigurationCache.Object, _jsonSerializer);
+
+ var result = factory.GetIndexValues(
+ propertyMock.Object,
+ "en-US",
+ null,
+ true,
+ [],
+ new Dictionary())
+ .ToList();
+
+ Assert.AreEqual(1, result.Count);
+ var indexValue = result.First();
+ Assert.AreEqual(indexValue.FieldName, "testAlias");
+ Assert.AreEqual(1, indexValue.Values.Count());
+ var value = indexValue.Values.First();
+ Assert.AreEqual(expectedIndexValue, value);
+ }
+}
From 6f85cc3c529caa8a91857bbeb6e0b077caad981d Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 22 Aug 2025 12:35:30 +0200
Subject: [PATCH 41/86] Improved picker validation
---
.../src/assets/lang/en.ts | 2 +
.../src/assets/lang/pt.ts | 2 +
...r-ui-date-with-time-zone-picker.element.ts | 156 ++++++++++++------
3 files changed, 107 insertions(+), 53 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
index 5be646d72e0d..787ada0cee09 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
@@ -2842,6 +2842,8 @@ export default {
config_timeZones_all: 'All - Display all available time zones',
config_timeZones_local: 'Local - Display only the local time zone',
config_timeZones_custom: 'Custom - Display a pre-defined list of time zones',
+ emptyTimeZone: 'Please select a time zone',
+ invalidTimeZone: 'The selected time zone is not valid',
},
uiCulture: {
ar: 'العربية',
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts
index c402f52f1cde..4198db2f507b 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts
@@ -2846,5 +2846,7 @@ export default {
config_timeZones_all: 'Todos - Mostrar todos os fusos horários disponÃveis',
config_timeZones_local: 'Local - Mostrar apenas o fuso horário local',
config_timeZones_custom: 'Personalizado - Mostrar uma lista pré-definida de fusos horários',
+ emptyTimeZone: 'Por favor, selecione um fuso horário',
+ invalidTimeZone: 'O fuso horário selecionado não é válido',
},
} as UmbLocalizationDictionary;
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
index 2beb4e5143d7..d340d88ea735 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
@@ -25,6 +25,12 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
import type { UmbDateTimeWithTimeZone, UmbTimeZonePickerValue } from '@umbraco-cms/backoffice/models';
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
+import {
+ UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
+ UmbFormControlMixin,
+ UmbValidationContext,
+} from '@umbraco-cms/backoffice/validation';
+import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
interface UmbTimeZonePickerOption extends UmbTimeZone {
offset: string;
@@ -35,27 +41,21 @@ interface UmbTimeZonePickerOption extends UmbTimeZone {
*/
@customElement('umb-property-editor-ui-date-with-time-zone-picker')
export class UmbPropertyEditorUIDateWithTimeZonePickerElement
- extends UmbLitElement
+ extends UmbFormControlMixin(UmbLitElement)
implements UmbPropertyEditorUiElement
{
private _timeZoneOptions: Array = [];
private _clientTimeZone: UmbTimeZone | undefined;
- @state()
- private _value: UmbDateTimeWithTimeZone | undefined;
-
- @property({ type: Object })
- public set value(value: UmbDateTimeWithTimeZone | undefined) {
- this._value = value;
- }
-
- public get value(): UmbDateTimeWithTimeZone | undefined {
- return this._value;
- }
-
@property({ type: Boolean, reflect: true })
readonly = false;
+ @property({ type: Boolean })
+ mandatory?: boolean;
+
+ @property({ type: String })
+ mandatoryMessage?: string | undefined;
+
@state()
private _dateInputType: InputDateType = 'datetime-local';
@@ -66,7 +66,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
private _dateInputStep: number = 1;
@state()
- private _currentDate?: DateTime;
+ private _selectedDate?: DateTime;
@state()
private _datePickerValue: string = '';
@@ -74,9 +74,64 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
@state()
private _filteredTimeZoneOptions: Array = [];
+ @state()
+ private _displayTimeZone: boolean = true;
+
@state()
private _selectedTimeZone: string | undefined;
+ readonly #validationContext = new UmbValidationContext(this);
+
+ /**
+ *
+ */
+ constructor() {
+ super();
+ this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
+ this.#gotPropertyContext(context);
+ });
+ }
+
+ #gotPropertyContext(context: typeof UMB_PROPERTY_CONTEXT.TYPE | undefined) {
+ this.observe(
+ context?.dataPath,
+ (dataPath) => {
+ if (dataPath) {
+ // Set the data path for the local validation context:
+ this.#validationContext.setDataPath(dataPath);
+ this.#validationContext.autoReport();
+ }
+ },
+ 'observeDataPath',
+ );
+ }
+
+ override connectedCallback() {
+ super.connectedCallback();
+
+ this.addValidator(
+ 'customError',
+ () => this.localize.term('timeZonePicker_emptyTimeZone'),
+ () => {
+ return (
+ (!!this.mandatory && !this.value?.date) || (this._displayTimeZone && !!(this.value && !this.value.timeZone))
+ );
+ },
+ );
+
+ this.addValidator(
+ 'customError',
+ () => this.localize.term('timeZonePicker_invalidTimeZone'),
+ () => {
+ return (
+ this._displayTimeZone &&
+ !!this.value?.timeZone &&
+ !this._timeZoneOptions.some((opt) => opt.value === this.value?.timeZone && !opt.invalid)
+ );
+ },
+ );
+ }
+
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
if (!config) return;
@@ -104,7 +159,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
}
#prefillValue(config: UmbPropertyEditorConfigCollection, includeTimeZones: boolean) {
- const date = this._value?.date;
+ const date = this.value?.date;
const zone = this.value?.timeZone;
if (!date) {
@@ -126,7 +181,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
return;
}
- this._currentDate = dateTime;
+ this._selectedDate = dateTime;
this._datePickerValue = dateTime.toFormat(this._dateInputFormat);
if (includeTimeZones) {
@@ -158,7 +213,8 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
break;
}
case 'custom': {
- this._timeZoneOptions = this._filteredTimeZoneOptions = getTimeZoneList(timeZonePickerConfig.timeZones).map((tz) => ({
+ this._timeZoneOptions = this._filteredTimeZoneOptions = getTimeZoneList(timeZonePickerConfig.timeZones).map(
+ (tz) => ({
...tz,
offset: getTimeZoneOffset(tz.value, dateToCalculateOffset),
invalid: false,
@@ -167,7 +223,8 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
const selectedTimeZone = this.value?.timeZone;
if (
selectedTimeZone &&
- !this._timeZoneOptions.some((opt) => isEquivalentTimeZone(opt.value, selectedTimeZone))) {
+ !this._timeZoneOptions.some((opt) => isEquivalentTimeZone(opt.value, selectedTimeZone))
+ ) {
// If the selected time zone is not in the list, we add it to the options
const customTimeZone: UmbTimeZonePickerOption = {
value: selectedTimeZone,
@@ -180,6 +237,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
break;
}
default:
+ this._displayTimeZone = false;
return;
}
@@ -212,9 +270,9 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
);
if (clientTimeZoneOpt) {
this._selectedTimeZone = clientTimeZoneOpt.value;
- if (this._currentDate) {
- this._currentDate = this._currentDate.setZone(clientTimeZone.value);
- this._datePickerValue = this._currentDate.toFormat(this._dateInputFormat);
+ if (this._selectedDate) {
+ this._selectedDate = this._selectedDate.setZone(clientTimeZone.value);
+ this._datePickerValue = this._selectedDate.toFormat(this._dateInputFormat);
}
return;
}
@@ -222,9 +280,9 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
// If no time zone was selected still, we can default to the first option
const firstOption = this._timeZoneOptions[0]?.value;
this._selectedTimeZone = firstOption;
- if (this._currentDate) {
- this._currentDate = this._currentDate.setZone(firstOption);
- this._datePickerValue = this._currentDate.toFormat(this._dateInputFormat);
+ if (this._selectedDate) {
+ this._selectedDate = this._selectedDate.setZone(firstOption);
+ this._datePickerValue = this._selectedDate.toFormat(this._dateInputFormat);
}
}
@@ -251,8 +309,8 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
if (!newPickerValue) {
this._datePickerValue = '';
- this._value = undefined;
- this._currentDate = undefined;
+ this.value = undefined;
+ this._selectedDate = undefined;
this.dispatchEvent(new UmbChangeEvent());
return;
}
@@ -270,16 +328,20 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
this._selectedTimeZone = timeZoneValue;
if (!this._selectedTimeZone) {
- this._value = undefined;
+ if (this.value?.date) {
+ this.value = { date: this.value.date, timeZone: undefined };
+ } else {
+ this.value = undefined;
+ }
this.dispatchEvent(new UmbChangeEvent());
return;
}
- if (!this._currentDate) {
+ if (!this._selectedDate) {
return;
}
- this.#updateValue(this._currentDate.toISO({ includeOffset: false }) || '');
+ this.#updateValue(this._selectedDate.toISO({ includeOffset: false }) || '');
}
#updateValue(date: string, updateOffsets = false) {
@@ -288,14 +350,14 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
// If the date is invalid, we reset the value
if (!newDate.isValid) {
- this._value = undefined;
- this._currentDate = undefined;
+ this.value = undefined;
+ this._selectedDate = undefined;
this.dispatchEvent(new UmbChangeEvent());
return;
}
- this._currentDate = newDate;
- this._value = {
+ this._selectedDate = newDate;
+ this.value = {
date: this.#getCurrentDateValue(),
timeZone: this._selectedTimeZone,
};
@@ -313,11 +375,11 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
#getCurrentDateValue(): string | undefined {
switch (this._dateInputType) {
case 'date':
- return this._currentDate?.toISODate() ?? undefined;
+ return this._selectedDate?.toISODate() ?? undefined;
case 'time':
- return this._currentDate?.toISOTime({ includeOffset: false }) ?? undefined;
+ return this._selectedDate?.toISOTime({ includeOffset: false }) ?? undefined;
default:
- return this._currentDate?.toISO({ includeOffset: !!this._selectedTimeZone }) ?? undefined;
+ return this._selectedDate?.toISO({ includeOffset: !!this._selectedTimeZone }) ?? undefined;
}
}
@@ -341,12 +403,12 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
${this.#renderTimeZones()}
- ${this.#renderTimeZoneInfo()} ${this.#renderTimeZoneValidation()}
+ ${this.#renderTimeZoneInfo()}
`;
}
#renderTimeZones() {
- if (this._timeZoneOptions.length === 0) {
+ if (!this._displayTimeZone || this._timeZoneOptions.length === 0) {
return nothing;
}
@@ -382,7 +444,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
if (
this._timeZoneOptions.length === 0 ||
!this._selectedTimeZone ||
- !this._currentDate ||
+ !this._selectedDate ||
this._selectedTimeZone === this._clientTimeZone?.value
) {
return nothing;
@@ -391,24 +453,12 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
return html`
${this.localize.term(
'timeZonePicker_differentTimeZoneLabel',
- `UTC${this._currentDate.toFormat('Z')}`,
- this._currentDate.toLocal().toFormat('ff'),
+ `UTC${this._selectedDate.toFormat('Z')}`,
+ this._selectedDate.toLocal().toFormat('ff'),
)}`;
}
- #renderTimeZoneValidation() {
- const selectionHasInvalids = this._timeZoneOptions.some((option) => {
- return option.invalid && option.value === this._selectedTimeZone;
- });
-
- if (selectionHasInvalids) {
- return html`
`;
- }
-
- return nothing;
- }
-
static override readonly styles = [
css`
:host {
From 2fce47d97257d8cbfec717ef498e960b634cb9e7 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 22 Aug 2025 12:37:52 +0200
Subject: [PATCH 42/86] Remove unused code
---
...r-ui-date-with-time-zone-picker.element.ts | 33 +------------------
1 file changed, 1 insertion(+), 32 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
index d340d88ea735..aa3d8ac77db9 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
@@ -25,12 +25,7 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
import type { UmbDateTimeWithTimeZone, UmbTimeZonePickerValue } from '@umbraco-cms/backoffice/models';
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
-import {
- UMB_VALIDATION_EMPTY_LOCALIZATION_KEY,
- UmbFormControlMixin,
- UmbValidationContext,
-} from '@umbraco-cms/backoffice/validation';
-import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
+import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
interface UmbTimeZonePickerOption extends UmbTimeZone {
offset: string;
@@ -80,32 +75,6 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
@state()
private _selectedTimeZone: string | undefined;
- readonly #validationContext = new UmbValidationContext(this);
-
- /**
- *
- */
- constructor() {
- super();
- this.consumeContext(UMB_PROPERTY_CONTEXT, (context) => {
- this.#gotPropertyContext(context);
- });
- }
-
- #gotPropertyContext(context: typeof UMB_PROPERTY_CONTEXT.TYPE | undefined) {
- this.observe(
- context?.dataPath,
- (dataPath) => {
- if (dataPath) {
- // Set the data path for the local validation context:
- this.#validationContext.setDataPath(dataPath);
- this.#validationContext.autoReport();
- }
- },
- 'observeDataPath',
- );
- }
-
override connectedCallback() {
super.connectedCallback();
From e3eeffe858913ad767f7a8320ed0ea01d3a61fb1 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 22 Aug 2025 12:52:22 +0200
Subject: [PATCH 43/86] Move models to their corresponding places
---
.../src/packages/core/models/index.ts | 10 ----------
...rty-editor-ui-date-with-time-zone-picker.element.ts | 8 +++++++-
.../property-editor-ui-time-zone-picker.element.ts | 6 +++++-
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts
index 565d5b2ff4c5..df910c6f2834 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts
@@ -33,13 +33,3 @@ export interface UmbUniqueItemModel {
name: string;
icon?: string;
}
-
-export interface UmbDateTimeWithTimeZone {
- date: string | undefined;
- timeZone: string | undefined;
-}
-
-export interface UmbTimeZonePickerValue {
- mode: 'all' | 'local' | 'custom';
- timeZones: Array
;
-}
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
index aa3d8ac77db9..d4729475f765 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
@@ -1,3 +1,4 @@
+import type { UmbTimeZonePickerValue } from '../time-zone-picker/property-editor-ui-time-zone-picker.element.js';
import {
getClientTimeZone,
getTimeZoneList,
@@ -23,14 +24,19 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { InputDateType, UmbInputDateElement } from '@umbraco-cms/backoffice/components';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui';
-import type { UmbDateTimeWithTimeZone, UmbTimeZonePickerValue } from '@umbraco-cms/backoffice/models';
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
+interface UmbDateTimeWithTimeZone {
+ date: string | undefined;
+ timeZone: string | undefined;
+}
+
interface UmbTimeZonePickerOption extends UmbTimeZone {
offset: string;
invalid: boolean;
}
+
/**
* @element umb-property-editor-ui-date-with-time-zone-picker
*/
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
index 57e9810baa9b..a32cc3a9f325 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/time-zone-picker/property-editor-ui-time-zone-picker.element.ts
@@ -6,10 +6,14 @@ import type {
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import type { UUIRadioEvent } from '@umbraco-cms/backoffice/external/uui';
-import type { UmbTimeZonePickerValue } from '@umbraco-cms/backoffice/models';
import { umbBindToValidation } from '@umbraco-cms/backoffice/validation';
import type { UmbInputTimeZoneElement } from '@umbraco-cms/backoffice/components';
+export interface UmbTimeZonePickerValue {
+ mode: 'all' | 'local' | 'custom';
+ timeZones: Array;
+}
+
/**
* @element umb-property-editor-ui-time-zone-picker
*/
From 22dc6ca318cb5b146fdb4689ebf1da338172dcf2 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 26 Aug 2025 15:14:44 +0200
Subject: [PATCH 44/86] Renaming `DateTimeWithTimeZone` to `DateTime2`
---
src/Umbraco.Core/Constants-DataTypes.cs | 18 ++-
src/Umbraco.Core/Constants-PropertyEditors.cs | 2 +-
.../PropertyEditors/DateTime2Configuration.cs | 82 +++++++++++
.../DateTime2ConfigurationEditor.cs | 14 ++
...tyEditor.cs => DateTime2PropertyEditor.cs} | 42 +++---
... => DateTime2PropertyIndexValueFactory.cs} | 8 +-
.../DateTimeWithTimeZoneConfiguration.cs | 82 -----------
...DateTimeWithTimeZoneConfigurationEditor.cs | 14 --
...=> IDateTime2PropertyIndexValueFactory.cs} | 2 +-
...onverter.cs => DateTime2ValueConverter.cs} | 48 +++----
.../UmbracoBuilder.CoreServices.cs | 2 +-
.../Migrations/Install/DatabaseDataCreator.cs | 45 +++++-
.../Umbraco.DateTime2.ts} | 6 +-
.../date-time-picker/manifests.ts | 18 +++
...rty-editor-ui-date-time-picker.element.ts} | 8 +-
.../date-with-time-zone-picker/manifests.ts | 18 ---
.../packages/property-editors/manifests.ts | 4 +-
...DateTime2PropertyIndexValueFactoryTest.cs} | 24 ++--
...sts.cs => DateTime2PropertyEditorTests.cs} | 88 ++++++------
.../DateTime2ValueConverterTests.cs | 132 ++++++++++++++++++
.../DateWithTimeZoneValueConverterTests.cs | 132 ------------------
21 files changed, 417 insertions(+), 372 deletions(-)
create mode 100644 src/Umbraco.Core/PropertyEditors/DateTime2Configuration.cs
create mode 100644 src/Umbraco.Core/PropertyEditors/DateTime2ConfigurationEditor.cs
rename src/Umbraco.Core/PropertyEditors/{DateTimeWithTimeZonePropertyEditor.cs => DateTime2PropertyEditor.cs} (77%)
rename src/Umbraco.Core/PropertyEditors/{DateTimeWithTimeZonePropertyIndexValueFactory.cs => DateTime2PropertyIndexValueFactory.cs} (78%)
delete mode 100644 src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfiguration.cs
delete mode 100644 src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfigurationEditor.cs
rename src/Umbraco.Core/PropertyEditors/{IDateTimeWithTimeZonePropertyIndexValueFactory.cs => IDateTime2PropertyIndexValueFactory.cs} (61%)
rename src/Umbraco.Core/PropertyEditors/ValueConverters/{DateTimeWithTimeZoneValueConverter.cs => DateTime2ValueConverter.cs} (64%)
rename src/Umbraco.Web.UI.Client/src/packages/property-editors/{date-with-time-zone-picker/Umbraco.DateTimeWithTimeZone.ts => date-time-picker/Umbraco.DateTime2.ts} (90%)
create mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/manifests.ts
rename src/Umbraco.Web.UI.Client/src/packages/property-editors/{date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts => date-time-picker/property-editor-ui-date-time-picker.element.ts} (98%)
delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/manifests.ts
rename tests/Umbraco.Tests.UnitTests/PropertyEditors/{DateTimeWithTimeZonePropertyIndexValueFactoryTest.cs => DateTime2PropertyIndexValueFactoryTest.cs} (63%)
rename tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/{DateWithTimeZonePropertyEditorTests.cs => DateTime2PropertyEditorTests.cs} (59%)
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
delete mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateWithTimeZoneValueConverterTests.cs
diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs
index ffcfef2c3795..7dcdce3707c0 100644
--- a/src/Umbraco.Core/Constants-DataTypes.cs
+++ b/src/Umbraco.Core/Constants-DataTypes.cs
@@ -116,9 +116,14 @@ public static class Guids
public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a";
///
- /// Guid for Date Time With Timezone as string
+ /// Guid for Date Time 2 (Unspecified) as string
///
- public const string DateTimeWithTimeZone = "88E8A052-30EE-4D44-A507-59F2CDFC769C";
+ public const string DateTime2Unspecified = "435B91BC-841B-4185-8402-5A8D462BCAF4";
+
+ ///
+ /// Guid for Date Time 2 With Timezone as string
+ ///
+ public const string DateTime2WithTimeZone = "88E8A052-30EE-4D44-A507-59F2CDFC769C";
///
/// Guid for Approved Color as string
@@ -296,9 +301,14 @@ public static class Guids
public static readonly Guid DatePickerWithTimeGuid = new(DatePickerWithTime);
///
- /// Guid for for Date Time With Timezone
+ /// Guid for Date Time 2 (Unspecified).
+ ///
+ public static readonly Guid DateTime2UnspecifiedGuid = new(DateTime2Unspecified);
+
+ ///
+ /// Guid for Date Time 2 (With Timezone).
///
- public static readonly Guid DateTimeWithTimeZoneGuid = new(DateTimeWithTimeZone);
+ public static readonly Guid DateTime2WithTimeZoneGuid = new(DateTime2WithTimeZone);
///
/// Guid for Approved Color
diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs
index a5a49311798e..00c1535bcf00 100644
--- a/src/Umbraco.Core/Constants-PropertyEditors.cs
+++ b/src/Umbraco.Core/Constants-PropertyEditors.cs
@@ -73,7 +73,7 @@ public static class Aliases
///
/// UTC DateTime.
///
- public const string DateTimeWithTimeZone = "Umbraco.DateTimeWithTimeZone";
+ public const string DateTime2 = "Umbraco.DateTime2";
///
/// DropDown List.
diff --git a/src/Umbraco.Core/PropertyEditors/DateTime2Configuration.cs b/src/Umbraco.Core/PropertyEditors/DateTime2Configuration.cs
new file mode 100644
index 000000000000..0c76d2ca50c4
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/DateTime2Configuration.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Text.Json.Serialization;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
+
+public class DateTime2Configuration
+{
+ ///
+ /// Gets or sets the format of the date and time value.
+ ///
+ [ConfigurationField("format")]
+ public DateTimeFormat Format { get; set; } = DateTimeFormat.DateTime;
+
+ ///
+ /// Gets or sets the time zones configuration.
+ ///
+ [ConfigurationField("timeZones")]
+ public TimeZonesConfiguration? TimeZones { get; set; }
+
+ public class TimeZonesConfiguration
+ {
+ ///
+ /// The mode for time zones.
+ ///
+ public TimeZoneMode? Mode { get; set; } = TimeZoneMode.None;
+
+ ///
+ /// A list of time zones to use when the mode is set to Custom.
+ ///
+ public List TimeZones { get; set; } = [];
+ }
+
+ public enum TimeZoneMode
+ {
+ ///
+ /// Do not display any time zones.
+ ///
+ [JsonStringEnumMemberName("none")]
+ None,
+
+ ///
+ /// Display all time zones.
+ ///
+ [JsonStringEnumMemberName("all")]
+ All,
+
+ ///
+ /// Display only the local time zone of the user.
+ ///
+ [JsonStringEnumMemberName("local")]
+ Local,
+
+ ///
+ /// Display a custom list of time zones defined in the configuration.
+ ///
+ [JsonStringEnumMemberName("custom")]
+ Custom,
+ }
+
+ public enum DateTimeFormat
+ {
+ ///
+ /// Display the date only, without time.
+ ///
+ [JsonStringEnumMemberName("date-only")]
+ DateOnly,
+
+ ///
+ /// Display the time only, without date.
+ ///
+ [JsonStringEnumMemberName("time-only")]
+ TimeOnly,
+
+ ///
+ /// Display both date and time.
+ ///
+ [JsonStringEnumMemberName("date-time")]
+ DateTime,
+ }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/DateTime2ConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTime2ConfigurationEditor.cs
new file mode 100644
index 000000000000..387f579c5d43
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/DateTime2ConfigurationEditor.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.IO;
+
+namespace Umbraco.Cms.Core.PropertyEditors;
+
+internal class DateTime2ConfigurationEditor : ConfigurationEditor
+{
+ public DateTime2ConfigurationEditor(IIOHelper ioHelper)
+ : base(ioHelper)
+ {
+ }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTime2PropertyEditor.cs
similarity index 77%
rename from src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
rename to src/Umbraco.Core/PropertyEditors/DateTime2PropertyEditor.cs
index f5266424396b..76f373b22338 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTime2PropertyEditor.cs
@@ -18,17 +18,17 @@
namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
- Constants.PropertyEditors.Aliases.DateTimeWithTimeZone,
+ Constants.PropertyEditors.Aliases.DateTime2,
ValueEditorIsReusable = true)]
-public class DateTimeWithTimeZonePropertyEditor : DataEditor
+public class DateTime2PropertyEditor : DataEditor
{
private readonly IIOHelper _ioHelper;
- private readonly IDateTimeWithTimeZonePropertyIndexValueFactory _propertyIndexValueFactory;
+ private readonly IDateTime2PropertyIndexValueFactory _propertyIndexValueFactory;
- public DateTimeWithTimeZonePropertyEditor(
+ public DateTime2PropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
- IDateTimeWithTimeZonePropertyIndexValueFactory propertyIndexValueFactory)
+ IDateTime2PropertyIndexValueFactory propertyIndexValueFactory)
: base(dataValueEditorFactory)
{
_ioHelper = ioHelper;
@@ -38,21 +38,21 @@ public DateTimeWithTimeZonePropertyEditor(
///
protected override IConfigurationEditor CreateConfigurationEditor() =>
- new DateTimeWithTimeZoneConfigurationEditor(_ioHelper);
+ new DateTime2ConfigurationEditor(_ioHelper);
///
protected override IDataValueEditor CreateValueEditor() =>
- DataValueEditorFactory.Create(Attribute!);
+ DataValueEditorFactory.Create(Attribute!);
///
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _propertyIndexValueFactory;
- internal class DateTimeWithTimeZoneDataValueEditor : DataValueEditor
+ internal class DateTime2DataValueEditor : DataValueEditor
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
- public DateTimeWithTimeZoneDataValueEditor(
+ public DateTime2DataValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
@@ -84,15 +84,15 @@ public DateTimeWithTimeZoneDataValueEditor(
return null;
}
- var configuration = editorValue.DataTypeConfiguration as DateTimeWithTimeZoneConfiguration;
- if (configuration?.Format == DateTimeWithTimeZoneFormat.TimeOnly)
+ var configuration = editorValue.DataTypeConfiguration as DateTime2Configuration;
+ if (configuration?.Format == DateTime2Configuration.DateTimeFormat.TimeOnly)
{
// Clear the date part if the format is TimeOnly.
// This is needed because `DateTimeOffset.TryParse` does not support `DateTimeStyles.NoCurrentDateDefault`.
dateTimeOffset = new DateTimeOffset(DateOnly.MinValue, TimeOnly.FromTimeSpan(dateTimeOffset.TimeOfDay), dateTimeOffset.Offset);
}
- var value = new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone
+ var value = new DateTime2ValueConverter.DateTime2
{
Date = dateTimeOffset,
TimeZone = valueAsJsonObject["timeZone"]?.GetValue(),
@@ -110,18 +110,18 @@ public DateTimeWithTimeZoneDataValueEditor(
return null;
}
- var interValue = DateTimeWithTimeZoneValueConverter.GetIntermediateValue(valueString, _jsonSerializer);
- if (interValue is not DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone dateWithTimeZone)
+ var interValue = DateTime2ValueConverter.GetIntermediateValue(valueString, _jsonSerializer);
+ if (interValue is not DateTime2ValueConverter.DateTime2 dateTime)
{
return null;
}
- DateTimeWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
- var objectValue = DateTimeWithTimeZoneValueConverter.GetObjectValue(dateWithTimeZone, configuration);
+ DateTime2Configuration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
+ var objectValue = DateTime2ValueConverter.GetObjectValue(dateTime, configuration);
JsonNode node = new JsonObject();
- node["date"] = DateTimeWithTimeZoneValueConverter.GetDateValueAsString(objectValue);
- node["timeZone"] = dateWithTimeZone.TimeZone;
+ node["date"] = DateTime2ValueConverter.GetDateValueAsString(objectValue);
+ node["timeZone"] = dateTime.TimeZone;
return node;
}
@@ -156,13 +156,13 @@ public IEnumerable Validate(
}
var selectedTimeZone = valueAsJsonObject["timeZone"]?.GetValue();
- var dataTypeConfig = dataTypeConfiguration as DateTimeWithTimeZoneConfiguration;
- if (dataTypeConfig?.TimeZones?.Mode is not { } mode || mode == DateTimeWithTimeZoneMode.None)
+ var dataTypeConfig = dataTypeConfiguration as DateTime2Configuration;
+ if (dataTypeConfig?.TimeZones?.Mode is not { } mode || mode == DateTime2Configuration.TimeZoneMode.None)
{
yield break;
}
- if (mode == DateTimeWithTimeZoneMode.Custom
+ if (mode == DateTime2Configuration.TimeZoneMode.Custom
&& dataTypeConfig.TimeZones.TimeZones.Any(t => t.Equals(selectedTimeZone, StringComparison.InvariantCultureIgnoreCase)) != true)
{
yield return new ValidationResult(
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/DateTime2PropertyIndexValueFactory.cs
similarity index 78%
rename from src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
rename to src/Umbraco.Core/PropertyEditors/DateTime2PropertyIndexValueFactory.cs
index 9abfb7859561..101799b545e0 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactory.cs
+++ b/src/Umbraco.Core/PropertyEditors/DateTime2PropertyIndexValueFactory.cs
@@ -6,12 +6,12 @@
namespace Umbraco.Cms.Core.PropertyEditors;
///
-public class DateTimeWithTimeZonePropertyIndexValueFactory : IDateTimeWithTimeZonePropertyIndexValueFactory
+public class DateTime2PropertyIndexValueFactory : IDateTime2PropertyIndexValueFactory
{
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
private readonly IJsonSerializer _jsonSerializer;
- public DateTimeWithTimeZonePropertyIndexValueFactory(
+ public DateTime2PropertyIndexValueFactory(
IDataTypeConfigurationCache dataTypeConfigurationCache,
IJsonSerializer jsonSerializer)
{
@@ -42,8 +42,8 @@ public IEnumerable GetIndexValues(
return [indexValue];
}
- DateTimeWithTimeZoneConfiguration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
- var value = DateTimeWithTimeZoneValueConverter.GetValue(sourceStr, configuration, _jsonSerializer);
+ DateTime2Configuration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
+ var value = DateTime2ValueConverter.GetValue(sourceStr, configuration, _jsonSerializer);
if (value is null)
{
return [indexValue];
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfiguration.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfiguration.cs
deleted file mode 100644
index 2c1606bc3535..000000000000
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfiguration.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (c) Umbraco.
-// See LICENSE for more details.
-
-using System.Text.Json.Serialization;
-
-namespace Umbraco.Cms.Core.PropertyEditors;
-
-public class DateTimeWithTimeZoneConfiguration
-{
- ///
- /// The format of the date and time value.
- ///
- [ConfigurationField("format")]
- public DateTimeWithTimeZoneFormat Format { get; set; } = DateTimeWithTimeZoneFormat.DateTime;
-
- ///
- /// The time zones configuration.
- ///
- [ConfigurationField("timeZones")]
- public DateTimeWithTimeZoneTimeZones? TimeZones { get; set; }
-}
-
-public class DateTimeWithTimeZoneTimeZones
-{
- ///
- /// The mode for time zones.
- ///
- public DateTimeWithTimeZoneMode? Mode { get; set; } = DateTimeWithTimeZoneMode.None;
-
- ///
- /// A list of time zones to use when the mode is set to Custom.
- ///
- public List TimeZones { get; set; } = [];
-}
-
-public enum DateTimeWithTimeZoneMode
-{
- ///
- /// Do not display any time zones.
- ///
- [JsonStringEnumMemberName("none")]
- None,
-
- ///
- /// Display all time zones.
- ///
- [JsonStringEnumMemberName("all")]
- All,
-
- ///
- /// Display only the local time zone of the user.
- ///
- [JsonStringEnumMemberName("local")]
- Local,
-
- ///
- /// Display a custom list of time zones defined in the configuration.
- ///
- [JsonStringEnumMemberName("custom")]
- Custom,
-}
-
-public enum DateTimeWithTimeZoneFormat
-{
- ///
- /// Display the date only, without time.
- ///
- [JsonStringEnumMemberName("date-only")]
- DateOnly,
-
- ///
- /// Display the time only, without date.
- ///
- [JsonStringEnumMemberName("time-only")]
- TimeOnly,
-
- ///
- /// Display both date and time.
- ///
- [JsonStringEnumMemberName("date-time")]
- DateTime,
-}
diff --git a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfigurationEditor.cs
deleted file mode 100644
index 0a863bfb5745..000000000000
--- a/src/Umbraco.Core/PropertyEditors/DateTimeWithTimeZoneConfigurationEditor.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Umbraco.
-// See LICENSE for more details.
-
-using Umbraco.Cms.Core.IO;
-
-namespace Umbraco.Cms.Core.PropertyEditors;
-
-internal class DateTimeWithTimeZoneConfigurationEditor : ConfigurationEditor
-{
- public DateTimeWithTimeZoneConfigurationEditor(IIOHelper ioHelper)
- : base(ioHelper)
- {
- }
-}
diff --git a/src/Umbraco.Core/PropertyEditors/IDateTimeWithTimeZonePropertyIndexValueFactory.cs b/src/Umbraco.Core/PropertyEditors/IDateTime2PropertyIndexValueFactory.cs
similarity index 61%
rename from src/Umbraco.Core/PropertyEditors/IDateTimeWithTimeZonePropertyIndexValueFactory.cs
rename to src/Umbraco.Core/PropertyEditors/IDateTime2PropertyIndexValueFactory.cs
index a5e7c01278dc..002491987546 100644
--- a/src/Umbraco.Core/PropertyEditors/IDateTimeWithTimeZonePropertyIndexValueFactory.cs
+++ b/src/Umbraco.Core/PropertyEditors/IDateTime2PropertyIndexValueFactory.cs
@@ -3,4 +3,4 @@ namespace Umbraco.Cms.Core.PropertyEditors;
///
/// Factory for creating index values for DateTime with TimeZone properties.
///
-public interface IDateTimeWithTimeZonePropertyIndexValueFactory : IPropertyIndexValueFactory;
+public interface IDateTime2PropertyIndexValueFactory : IPropertyIndexValueFactory;
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
similarity index 64%
rename from src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
rename to src/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
index f962a6641f42..5efe40921e1a 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTimeWithTimeZoneValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
@@ -9,26 +9,26 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// The date time with timezone property value converter.
///
[DefaultPropertyValueConverter]
-public class DateTimeWithTimeZoneValueConverter : PropertyValueConverterBase
+public class DateTime2ValueConverter : PropertyValueConverterBase
{
private readonly IJsonSerializer _jsonSerializer;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The JSON serializer.
- public DateTimeWithTimeZoneValueConverter(IJsonSerializer jsonSerializer)
+ public DateTime2ValueConverter(IJsonSerializer jsonSerializer)
=> _jsonSerializer = jsonSerializer;
///
public override bool IsConverter(IPublishedPropertyType propertyType)
- => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTimeWithTimeZone);
+ => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.DateTime2);
///
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
{
- DateTimeWithTimeZoneConfiguration? config =
- ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
+ DateTime2Configuration? config =
+ ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
return GetPropertyValueType(config);
}
@@ -44,12 +44,12 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
bool preview)
{
var sourceStr = source?.ToString();
- if (sourceStr is null || !_jsonSerializer.TryDeserialize(sourceStr, out DateTimeWithTimeZone? dateWithTimeZone))
+ if (sourceStr is null || !_jsonSerializer.TryDeserialize(sourceStr, out DateTime2? dateTime))
{
return null;
}
- return dateWithTimeZone;
+ return dateTime;
}
public override object? ConvertIntermediateToObject(
@@ -59,31 +59,31 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
object? inter,
bool preview)
{
- DateTimeWithTimeZoneConfiguration? config =
- ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
+ DateTime2Configuration? config =
+ ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
return GetObjectValue(inter, config);
}
- private static Type GetPropertyValueType(DateTimeWithTimeZoneConfiguration? config) =>
+ private static Type GetPropertyValueType(DateTime2Configuration? config) =>
config?.Format switch
{
- DateTimeWithTimeZoneFormat.DateOnly => typeof(DateOnly?),
- DateTimeWithTimeZoneFormat.TimeOnly => typeof(TimeOnly?),
- DateTimeWithTimeZoneFormat.DateTime when config.TimeZones?.Mode is not { } mode || mode == DateTimeWithTimeZoneMode.None => typeof(DateTime?),
+ DateTime2Configuration.DateTimeFormat.DateOnly => typeof(DateOnly?),
+ DateTime2Configuration.DateTimeFormat.TimeOnly => typeof(TimeOnly?),
+ DateTime2Configuration.DateTimeFormat.DateTime when config.TimeZones?.Mode is not { } mode || mode == DateTime2Configuration.TimeZoneMode.None => typeof(DateTime?),
_ => typeof(DateTimeOffset?),
};
internal static object? GetIntermediateValue(string? source, IJsonSerializer jsonSerializer)
{
- if (source is null || !jsonSerializer.TryDeserialize(source, out DateTimeWithTimeZone? dateWithTimeZone))
+ if (source is null || !jsonSerializer.TryDeserialize(source, out DateTime2? dateTime))
{
return null;
}
- return dateWithTimeZone;
+ return dateTime;
}
- internal static object? GetObjectValue(object? inter, DateTimeWithTimeZoneConfiguration? configuration)
+ internal static object? GetObjectValue(object? inter, DateTime2Configuration? configuration)
{
Type propertyValueType = GetPropertyValueType(configuration);
if (inter is null)
@@ -91,30 +91,30 @@ private static Type GetPropertyValueType(DateTimeWithTimeZoneConfiguration? conf
return propertyValueType.GetDefaultValue();
}
- if (inter is not DateTimeWithTimeZone dateWithTimeZone)
+ if (inter is not DateTime2 dateTime)
{
return propertyValueType.GetDefaultValue();
}
if (propertyValueType == typeof(DateOnly?))
{
- return DateOnly.FromDateTime(dateWithTimeZone.Date.UtcDateTime);
+ return DateOnly.FromDateTime(dateTime.Date.UtcDateTime);
}
if (propertyValueType == typeof(TimeOnly?))
{
- return TimeOnly.FromDateTime(dateWithTimeZone.Date.UtcDateTime);
+ return TimeOnly.FromDateTime(dateTime.Date.UtcDateTime);
}
if (propertyValueType == typeof(DateTime?))
{
- return DateTime.SpecifyKind(dateWithTimeZone.Date.UtcDateTime, DateTimeKind.Unspecified);
+ return DateTime.SpecifyKind(dateTime.Date.UtcDateTime, DateTimeKind.Unspecified);
}
- return dateWithTimeZone.Date;
+ return dateTime.Date;
}
- internal static object? GetValue(string? source, DateTimeWithTimeZoneConfiguration? configuration, IJsonSerializer jsonSerializer)
+ internal static object? GetValue(string? source, DateTime2Configuration? configuration, IJsonSerializer jsonSerializer)
{
var intermediateValue = GetIntermediateValue(source, jsonSerializer);
return GetObjectValue(intermediateValue, configuration);
@@ -128,7 +128,7 @@ private static Type GetPropertyValueType(DateTimeWithTimeZoneConfiguration? conf
_ => throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported type: {value.GetType().FullName}"),
};
- public class DateTimeWithTimeZone
+ public class DateTime2
{
[JsonPropertyName("date")]
public DateTimeOffset Date { get; init; }
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
index 4e1071649945..ddc3fefe6cfa 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs
@@ -263,7 +263,7 @@ public static IUmbracoBuilder AddPropertyIndexValueFactories(this IUmbracoBuilde
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
return builder;
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index f2e749c97755..3a8baec1956d 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -859,7 +859,7 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text)
"id");
ConditionalInsert(
Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes,
- Constants.DataTypes.Guids.DateTimeWithTimeZone,
+ Constants.DataTypes.Guids.DateTime2Unspecified,
new NodeDto
{
NodeId = 1055,
@@ -869,8 +869,27 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text)
Level = 1,
Path = "-1,1055",
SortOrder = 2,
- UniqueId = Constants.DataTypes.Guids.DateTimeWithTimeZoneGuid,
- Text = "Date Picker with Time Zone",
+ UniqueId = Constants.DataTypes.Guids.DateTime2UnspecifiedGuid,
+ Text = "Date Time (Unspecified)",
+ NodeObjectType = Constants.ObjectTypes.DataType,
+ CreateDate = DateTime.Now,
+ },
+ Constants.DatabaseSchema.Tables.Node,
+ "id");
+ ConditionalInsert(
+ Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes,
+ Constants.DataTypes.Guids.DateTime2WithTimeZone,
+ new NodeDto
+ {
+ NodeId = 1056,
+ Trashed = false,
+ ParentId = -1,
+ UserId = -1,
+ Level = 1,
+ Path = "-1,1056",
+ SortOrder = 2,
+ UniqueId = Constants.DataTypes.Guids.DateTime2WithTimeZoneGuid,
+ Text = "Date Time (With Time Zone)",
NodeObjectType = Constants.ObjectTypes.DataType,
CreateDate = DateTime.Now,
},
@@ -2321,8 +2340,24 @@ string GridCollectionView(string collectionViewType) =>
new DataTypeDto
{
NodeId = 1055,
- EditorAlias = Constants.PropertyEditors.Aliases.DateTimeWithTimeZone,
- EditorUiAlias = "Umb.PropertyEditorUi.DateWithTimeZonePicker",
+ EditorAlias = Constants.PropertyEditors.Aliases.DateTime2,
+ EditorUiAlias = "Umb.PropertyEditorUi.DateTimePicker",
+ DbType = "Ntext",
+ Configuration = "{\"format\": \"date-time\", \"timeFormat\": \"HH:mm\", \"timeZones\": {\"mode\": \"none\"}}",
+ });
+ }
+
+ if (_database.Exists(1056))
+ {
+ _database.Insert(
+ Constants.DatabaseSchema.Tables.DataType,
+ "pk",
+ false,
+ new DataTypeDto
+ {
+ NodeId = 1056,
+ EditorAlias = Constants.PropertyEditors.Aliases.DateTime2,
+ EditorUiAlias = "Umb.PropertyEditorUi.DateTimePicker",
DbType = "Ntext",
Configuration = "{\"format\": \"date-time\", \"timeFormat\": \"HH:mm\", \"timeZones\": {\"mode\": \"all\"}}",
});
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/Umbraco.DateTimeWithTimeZone.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/Umbraco.DateTime2.ts
similarity index 90%
rename from src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/Umbraco.DateTimeWithTimeZone.ts
rename to src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/Umbraco.DateTime2.ts
index d7f12f67b34a..18d65ea68992 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/Umbraco.DateTimeWithTimeZone.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/Umbraco.DateTime2.ts
@@ -2,10 +2,10 @@ import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/prope
export const manifest: ManifestPropertyEditorSchema = {
type: 'propertyEditorSchema',
- name: 'Date Time with Time Zone',
- alias: 'Umbraco.DateTimeWithTimeZone',
+ name: 'Date Time 2',
+ alias: 'Umbraco.DateTime2',
meta: {
- defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.DateWithTimeZonePicker',
+ defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.DateTimePicker',
settings: {
properties: [
{
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/manifests.ts
new file mode 100644
index 000000000000..fa105ab10190
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/manifests.ts
@@ -0,0 +1,18 @@
+import { manifest as schemaManifest } from './Umbraco.DateTime2.js';
+
+export const manifests: Array = [
+ {
+ type: 'propertyEditorUi',
+ alias: 'Umb.PropertyEditorUi.DateTimePicker',
+ name: 'Date Time Picker Property Editor UI',
+ element: () => import('./property-editor-ui-date-time-picker.element.js'),
+ meta: {
+ label: 'Date Time Picker',
+ propertyEditorSchemaAlias: 'Umbraco.DateTime2',
+ icon: 'icon-time',
+ group: 'pickers',
+ supportsReadOnly: true,
+ },
+ },
+ schemaManifest,
+];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/property-editor-ui-date-time-picker.element.ts
similarity index 98%
rename from src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
rename to src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/property-editor-ui-date-time-picker.element.ts
index d4729475f765..065f92a39129 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/property-editor-ui-date-with-time-zone-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/property-editor-ui-date-time-picker.element.ts
@@ -27,7 +27,7 @@ import type { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffi
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
-interface UmbDateTimeWithTimeZone {
+interface UmbDateTime2 {
date: string | undefined;
timeZone: string | undefined;
}
@@ -38,11 +38,11 @@ interface UmbTimeZonePickerOption extends UmbTimeZone {
}
/**
- * @element umb-property-editor-ui-date-with-time-zone-picker
+ * @element umb-property-editor-ui-date-time-picker
*/
-@customElement('umb-property-editor-ui-date-with-time-zone-picker')
+@customElement('umb-property-editor-ui-date-time-picker')
export class UmbPropertyEditorUIDateWithTimeZonePickerElement
- extends UmbFormControlMixin(UmbLitElement)
+ extends UmbFormControlMixin(UmbLitElement)
implements UmbPropertyEditorUiElement
{
private _timeZoneOptions: Array = [];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/manifests.ts
deleted file mode 100644
index d72b594a03ce..000000000000
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-with-time-zone-picker/manifests.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { manifest as schemaManifest } from './Umbraco.DateTimeWithTimeZone.js';
-
-export const manifests: Array = [
- {
- type: 'propertyEditorUi',
- alias: 'Umb.PropertyEditorUi.DateWithTimeZonePicker',
- name: 'Date With Time Zone Picker Property Editor UI',
- element: () => import('./property-editor-ui-date-with-time-zone-picker.element.js'),
- meta: {
- label: 'Date With Time Zone Picker',
- propertyEditorSchemaAlias: 'Umbraco.DateTimeWithTimeZone',
- icon: 'icon-time',
- group: 'pickers',
- supportsReadOnly: true,
- },
- },
- schemaManifest,
-];
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts
index 65d73e1d3af1..208e5268aa5b 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts
@@ -11,7 +11,7 @@ import { manifests as checkboxListManifests } from './checkbox-list/manifests.js
import { manifests as collectionManifests } from './collection/manifests.js';
import { manifests as colorPickerManifests } from './color-picker/manifests.js';
import { manifests as datePickerManifests } from './date-picker/manifests.js';
-import { manifests as dateWithTimeZonePickerManifests } from './date-with-time-zone-picker/manifests.js';
+import { manifests as dateTimePickerManifests } from './date-time-picker/manifests.js';
import { manifests as dropdownManifests } from './dropdown/manifests.js';
import { manifests as eyeDropperManifests } from './eye-dropper/manifests.js';
import { manifests as iconPickerManifests } from './icon-picker/manifests.js';
@@ -30,7 +30,7 @@ export const manifests: Array = [
...collectionManifests,
...colorPickerManifests,
...datePickerManifests,
- ...dateWithTimeZonePickerManifests,
+ ...dateTimePickerManifests,
...dropdownManifests,
...eyeDropperManifests,
...iconPickerManifests,
diff --git a/tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactoryTest.cs b/tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTime2PropertyIndexValueFactoryTest.cs
similarity index 63%
rename from tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactoryTest.cs
rename to tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTime2PropertyIndexValueFactoryTest.cs
index 1c07df455ff2..cc2d51f70394 100644
--- a/tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTimeWithTimeZonePropertyIndexValueFactoryTest.cs
+++ b/tests/Umbraco.Tests.UnitTests/PropertyEditors/DateTime2PropertyIndexValueFactoryTest.cs
@@ -9,8 +9,8 @@
namespace Umbraco.Cms.Tests.UnitTests.PropertyEditors;
[TestFixture]
-[TestOf(typeof(DateTimeWithTimeZonePropertyIndexValueFactory))]
-public class DateTimeWithTimeZonePropertyIndexValueFactoryTest
+[TestOf(typeof(DateTime2PropertyIndexValueFactory))]
+public class DateTime2PropertyIndexValueFactoryTest
{
private readonly IJsonSerializer _jsonSerializer =
new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory());
@@ -25,7 +25,7 @@ public void GetIndexValues_ReturnsEmptyValues_ForNullPropertyValue()
.Returns("testAlias");
propertyMock.Setup(x => x.GetValue("en-US", null, true))
.Returns(null);
- var factory = new DateTimeWithTimeZonePropertyIndexValueFactory(_dataTypeConfigurationCache.Object, _jsonSerializer);
+ var factory = new DateTime2PropertyIndexValueFactory(_dataTypeConfigurationCache.Object, _jsonSerializer);
var result = factory.GetIndexValues(
propertyMock.Object,
@@ -42,11 +42,11 @@ public void GetIndexValues_ReturnsEmptyValues_ForNullPropertyValue()
Assert.IsEmpty(indexValue.Values);
}
- [TestCase(DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.All, "{\"date\":\"2023-01-18T12:00:00+01:00\",\"timeZone\":\"Europe/Copenhagen\"}", "2023-01-18T11:00:00.0000000Z")]
- [TestCase(DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.None, "{\"date\":\"2023-01-18T12:00:00Z\",\"timeZone\":\"Europe/Copenhagen\"}", "2023-01-18T12:00:00.0000000")]
- [TestCase(DateTimeWithTimeZoneFormat.DateOnly, DateTimeWithTimeZoneMode.None, "{\"date\":\"2023-01-18T00:00:00Z\",\"timeZone\":null}", "2023-01-18")]
- [TestCase(DateTimeWithTimeZoneFormat.TimeOnly, DateTimeWithTimeZoneMode.None, "{\"date\":\"0001-01-01T12:00:00Z\",\"timeZone\":null}", "12:00:00.0000000")]
- public void GetIndexValues_ReturnsFormattedUtcDateTime(DateTimeWithTimeZoneFormat format, DateTimeWithTimeZoneMode mode, string propertyValue, string expectedIndexValue)
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.All, "{\"date\":\"2023-01-18T12:00:00+01:00\",\"timeZone\":\"Europe/Copenhagen\"}", "2023-01-18T11:00:00.0000000Z")]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.None, "{\"date\":\"2023-01-18T12:00:00Z\",\"timeZone\":\"Europe/Copenhagen\"}", "2023-01-18T12:00:00.0000000")]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.None, "{\"date\":\"2023-01-18T00:00:00Z\",\"timeZone\":null}", "2023-01-18")]
+ [TestCase(DateTime2Configuration.DateTimeFormat.TimeOnly, DateTime2Configuration.TimeZoneMode.None, "{\"date\":\"0001-01-01T12:00:00Z\",\"timeZone\":null}", "12:00:00.0000000")]
+ public void GetIndexValues_ReturnsFormattedUtcDateTime(DateTime2Configuration.DateTimeFormat format, DateTime2Configuration.TimeZoneMode mode, string propertyValue, string expectedIndexValue)
{
var dataTypeKey = Guid.NewGuid();
var propertyMock = new Mock(MockBehavior.Strict);
@@ -57,15 +57,15 @@ public void GetIndexValues_ReturnsFormattedUtcDateTime(DateTimeWithTimeZoneForma
propertyMock.Setup(x => x.GetValue("en-US", null, true))
.Returns(propertyValue);
- var configuration = new DateTimeWithTimeZoneConfiguration
+ var configuration = new DateTime2Configuration
{
Format = format,
- TimeZones = new DateTimeWithTimeZoneTimeZones { Mode = mode },
+ TimeZones = new DateTime2Configuration.TimeZonesConfiguration { Mode = mode },
};
- _dataTypeConfigurationCache.Setup(x => x.GetConfigurationAs(dataTypeKey))
+ _dataTypeConfigurationCache.Setup(x => x.GetConfigurationAs(dataTypeKey))
.Returns(configuration);
- var factory = new DateTimeWithTimeZonePropertyIndexValueFactory(_dataTypeConfigurationCache.Object, _jsonSerializer);
+ var factory = new DateTime2PropertyIndexValueFactory(_dataTypeConfigurationCache.Object, _jsonSerializer);
var result = factory.GetIndexValues(
propertyMock.Object,
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateWithTimeZonePropertyEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
similarity index 59%
rename from tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateWithTimeZonePropertyEditorTests.cs
rename to tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
index 0582e2c5e7e0..a7123e06c4db 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateWithTimeZonePropertyEditorTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
@@ -18,7 +18,7 @@
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
[TestFixture]
-public class DateWithTimeZonePropertyEditorTests
+public class DateTime2PropertyEditorTests
{
private readonly IJsonSerializer _jsonSerializer =
new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory());
@@ -56,23 +56,23 @@ public void Validates_Date_Received(object? value, bool expectedSuccess)
private static readonly object[] _sourceList2 =
[
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTimeWithTimeZoneMode.None, Array.Empty(), true },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTimeWithTimeZoneMode.All, Array.Empty(), true },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTimeWithTimeZoneMode.Local, Array.Empty(), true },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTimeWithTimeZoneMode.Custom, new[] { "Europe/Copenhagen" }, false },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.None, Array.Empty(), true },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.All, Array.Empty(), true },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Local, Array.Empty(), true },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Custom, Array.Empty(), false },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Custom, new[] { "Europe/Copenhagen" }, true },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Custom, new[] { "Europe/Amsterdam", "Europe/Copenhagen" }, true },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTimeWithTimeZoneMode.Custom, new[] { "Europe/Amsterdam" }, false },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTime2Configuration.TimeZoneMode.None, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTime2Configuration.TimeZoneMode.All, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTime2Configuration.TimeZoneMode.Local, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\"}"), DateTime2Configuration.TimeZoneMode.Custom, new[] { "Europe/Copenhagen" }, false },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTime2Configuration.TimeZoneMode.None, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTime2Configuration.TimeZoneMode.All, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTime2Configuration.TimeZoneMode.Local, Array.Empty(), true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTime2Configuration.TimeZoneMode.Custom, Array.Empty(), false },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTime2Configuration.TimeZoneMode.Custom, new[] { "Europe/Copenhagen" }, true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTime2Configuration.TimeZoneMode.Custom, new[] { "Europe/Amsterdam", "Europe/Copenhagen" }, true },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T14:30:00\", \"timeZone\": \"Europe/Copenhagen\"}"), DateTime2Configuration.TimeZoneMode.Custom, new[] { "Europe/Amsterdam" }, false },
];
[TestCaseSource(nameof(_sourceList2))]
public void Validates_TimeZone_Received(
object value,
- DateTimeWithTimeZoneMode timeZoneMode,
+ DateTime2Configuration.TimeZoneMode timeZoneMode,
string[] timeZones,
bool expectedSuccess)
{
@@ -93,25 +93,25 @@ public void Validates_TimeZone_Received(
private static readonly object[] _sourceList3 =
[
- new object[] { null, DateTimeWithTimeZoneFormat.DateOnly, null, null },
- new object[] { JsonNode.Parse("{}"), DateTimeWithTimeZoneFormat.DateTime, null, null },
- new object[] { JsonNode.Parse("{\"INVALID\": \"\"}"), DateTimeWithTimeZoneFormat.DateTime, null, null },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20\"}"), DateTimeWithTimeZoneFormat.DateOnly, new DateTimeOffset(2025, 8, 20, 0, 0, 0, TimeSpan.Zero), null },
- new object[] { JsonNode.Parse("{\"date\": \"16:34\"}"), DateTimeWithTimeZoneFormat.TimeOnly, new DateTimeOffset(1, 1, 1, 16, 34, 0, TimeSpan.Zero), null },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01\"}"), DateTimeWithTimeZoneFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.Zero), null },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01Z\"}"), DateTimeWithTimeZoneFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.Zero), null },
- new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01-05:00\"}"), DateTimeWithTimeZoneFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.FromHours(-5)), null },
+ new object[] { null, DateTime2Configuration.DateTimeFormat.DateOnly, null, null },
+ new object[] { JsonNode.Parse("{}"), DateTime2Configuration.DateTimeFormat.DateTime, null, null },
+ new object[] { JsonNode.Parse("{\"INVALID\": \"\"}"), DateTime2Configuration.DateTimeFormat.DateTime, null, null },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20\"}"), DateTime2Configuration.DateTimeFormat.DateOnly, new DateTimeOffset(2025, 8, 20, 0, 0, 0, TimeSpan.Zero), null },
+ new object[] { JsonNode.Parse("{\"date\": \"16:34\"}"), DateTime2Configuration.DateTimeFormat.TimeOnly, new DateTimeOffset(1, 1, 1, 16, 34, 0, TimeSpan.Zero), null },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01\"}"), DateTime2Configuration.DateTimeFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.Zero), null },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01Z\"}"), DateTime2Configuration.DateTimeFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.Zero), null },
+ new object[] { JsonNode.Parse("{\"date\": \"2025-08-20T18:30:01-05:00\"}"), DateTime2Configuration.DateTimeFormat.DateTime, new DateTimeOffset(2025, 8, 20, 18, 30, 1, TimeSpan.FromHours(-5)), null },
];
[TestCaseSource(nameof(_sourceList3))]
public void Can_Parse_Values_From_Editor(
object? value,
- DateTimeWithTimeZoneFormat format,
+ DateTime2Configuration.DateTimeFormat format,
DateTimeOffset? expectedDateTimeOffset,
string? expectedTimeZone)
{
var expectedJson = expectedDateTimeOffset is null ? null : _jsonSerializer.Serialize(
- new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone
+ new DateTime2ValueConverter.DateTime2
{
Date = expectedDateTimeOffset.Value,
TimeZone = expectedTimeZone,
@@ -119,12 +119,12 @@ public void Can_Parse_Values_From_Editor(
var result = CreateValueEditor(format: format).FromEditor(
new ContentPropertyData(
value,
- new DateTimeWithTimeZoneConfiguration
+ new DateTime2Configuration
{
Format = format,
- TimeZones = new DateTimeWithTimeZoneTimeZones
+ TimeZones = new DateTime2Configuration.TimeZonesConfiguration
{
- Mode = DateTimeWithTimeZoneMode.None,
+ Mode = DateTime2Configuration.TimeZoneMode.None,
TimeZones = [],
},
}),
@@ -134,25 +134,25 @@ public void Can_Parse_Values_From_Editor(
private static readonly object[][] _sourceList4 =
[
- [null, DateTimeWithTimeZoneFormat.DateOnly, null],
- [null, DateTimeWithTimeZoneFormat.TimeOnly, null],
- [null, DateTimeWithTimeZoneFormat.DateTime, null],
- [new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTimeWithTimeZoneFormat.DateOnly, "{\"date\":\"2025-08-20\",\"timeZone\":null}"],
- [new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTimeWithTimeZoneFormat.TimeOnly, "{\"date\":\"16:30:00.0000000\",\"timeZone\":null}"],
- [new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTimeWithTimeZoneFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":null}"],
- [new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.FromHours(-5)) }, DateTimeWithTimeZoneFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":null}"],
- [new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" }, DateTimeWithTimeZoneFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":\"Europe/Copenhagen\"}"],
+ [null, DateTime2Configuration.DateTimeFormat.DateOnly, null],
+ [null, DateTime2Configuration.DateTimeFormat.TimeOnly, null],
+ [null, DateTime2Configuration.DateTimeFormat.DateTime, null],
+ [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTime2Configuration.DateTimeFormat.DateOnly, "{\"date\":\"2025-08-20\",\"timeZone\":null}"],
+ [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTime2Configuration.DateTimeFormat.TimeOnly, "{\"date\":\"16:30:00.0000000\",\"timeZone\":null}"],
+ [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTime2Configuration.DateTimeFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":null}"],
+ [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.FromHours(-5)) }, DateTime2Configuration.DateTimeFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":null}"],
+ [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" }, DateTime2Configuration.DateTimeFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":\"Europe/Copenhagen\"}"],
];
[TestCaseSource(nameof(_sourceList4))]
public void Can_Parse_Values_To_Editor(
- DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone? storedValue,
- DateTimeWithTimeZoneFormat format,
+ DateTime2ValueConverter.DateTime2? storedValue,
+ DateTime2Configuration.DateTimeFormat format,
string? expectedJson)
{
var valueEditor = CreateValueEditor(format: format);
- _dataTypeConfigurationCache.Setup(dc => dc.GetConfigurationAs(Guid.Empty))
- .Returns(valueEditor.ConfigurationObject as DateTimeWithTimeZoneConfiguration);
+ _dataTypeConfigurationCache.Setup(dc => dc.GetConfigurationAs(Guid.Empty))
+ .Returns(valueEditor.ConfigurationObject as DateTime2Configuration);
var storedValueJson = storedValue is null ? null : _jsonSerializer.Serialize(storedValue);
var result = valueEditor.ToEditor(
new Property(new PropertyType(Mock.Of(), "dataType", ValueStorageType.Ntext))
@@ -176,9 +176,9 @@ public void Can_Parse_Values_To_Editor(
Assert.AreEqual(expectedJson, (result as JsonNode)?.AsJsonString());
}
- private DateTimeWithTimeZonePropertyEditor.DateTimeWithTimeZoneDataValueEditor CreateValueEditor(
- DateTimeWithTimeZoneFormat format = DateTimeWithTimeZoneFormat.DateTime,
- DateTimeWithTimeZoneMode timeZoneMode = DateTimeWithTimeZoneMode.All,
+ private DateTime2PropertyEditor.DateTime2DataValueEditor CreateValueEditor(
+ DateTime2Configuration.DateTimeFormat format = DateTime2Configuration.DateTimeFormat.DateTime,
+ DateTime2Configuration.TimeZoneMode timeZoneMode = DateTime2Configuration.TimeZoneMode.All,
string[]? timeZones = null)
{
var localizedTextServiceMock = new Mock();
@@ -188,7 +188,7 @@ private DateTimeWithTimeZonePropertyEditor.DateTimeWithTimeZoneDataValueEditor C
It.IsAny(),
It.IsAny>()))
.Returns((string key, string alias, CultureInfo _, IDictionary _) => $"{key}_{alias}");
- var valueEditor = new DateTimeWithTimeZonePropertyEditor.DateTimeWithTimeZoneDataValueEditor(
+ var valueEditor = new DateTime2PropertyEditor.DateTime2DataValueEditor(
Mock.Of(),
_jsonSerializer,
Mock.Of(),
@@ -196,10 +196,10 @@ private DateTimeWithTimeZonePropertyEditor.DateTimeWithTimeZoneDataValueEditor C
localizedTextServiceMock.Object,
_dataTypeConfigurationCache.Object)
{
- ConfigurationObject = new DateTimeWithTimeZoneConfiguration
+ ConfigurationObject = new DateTime2Configuration
{
Format = format,
- TimeZones = new DateTimeWithTimeZoneTimeZones
+ TimeZones = new DateTime2Configuration.TimeZonesConfiguration
{
Mode = timeZoneMode,
TimeZones = timeZones?.ToList() ?? [],
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
new file mode 100644
index 000000000000..ec7e97904ddf
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
@@ -0,0 +1,132 @@
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.PropertyEditors;
+using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Infrastructure.Serialization;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors.ValueConverters;
+
+[TestFixture]
+public class DateTime2ValueConverterTests
+{
+ private readonly IJsonSerializer _jsonSerializer =
+ new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory());
+
+ [TestCase(Constants.PropertyEditors.Aliases.DateTime2, true)]
+ [TestCase(Constants.PropertyEditors.Aliases.DateTime, false)]
+ public void IsConverter_For(string propertyEditorAlias, bool expected)
+ {
+ var propertyType = Mock.Of(x => x.EditorAlias == propertyEditorAlias);
+ var converter = new DateTime2ValueConverter(Mock.Of(MockBehavior.Strict));
+
+ var result = converter.IsConverter(propertyType);
+
+ Assert.AreEqual(expected, result);
+ }
+
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.None, typeof(DateOnly?))]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.All, typeof(DateOnly?))]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.Custom, typeof(DateOnly?))]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.Local, typeof(DateOnly?))]
+ [TestCase(DateTime2Configuration.DateTimeFormat.TimeOnly, DateTime2Configuration.TimeZoneMode.None, typeof(TimeOnly?))]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.None, typeof(DateTime?))]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.All, typeof(DateTimeOffset?))]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.Custom, typeof(DateTimeOffset?))]
+ [TestCase(DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.Local, typeof(DateTimeOffset?))]
+ public void GetPropertyValueType_ReturnsExpectedType(DateTime2Configuration.DateTimeFormat format, DateTime2Configuration.TimeZoneMode timeZoneMode, Type expectedType)
+ {
+ var converter = new DateTime2ValueConverter(Mock.Of(MockBehavior.Strict));
+ var dataType = new PublishedDataType(
+ 0,
+ "test",
+ "test",
+ new Lazy(() =>
+ new DateTime2Configuration
+ {
+ Format = format,
+ TimeZones = new DateTime2Configuration.TimeZonesConfiguration { Mode = timeZoneMode },
+ }));
+ var propertyType = Mock.Of(x => x.DataType == dataType);
+
+ var result = converter.GetPropertyValueType(propertyType);
+
+ Assert.AreEqual(expectedType, result);
+ }
+
+ private static object[] _convertToIntermediateCases =
+ [
+ new object[] { null, null },
+ new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":null}", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = null } },
+ new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":\"Europe/Copenhagen\"}", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = "Europe/Copenhagen" } },
+ new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":\"Europe/Copenhagen\"}", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" } },
+ ];
+
+ [TestCaseSource(nameof(_convertToIntermediateCases))]
+ public void Can_Convert_To_Intermediate_Value(string? input, DateTime2ValueConverter.DateTime2? expected)
+ {
+ var result = new DateTime2ValueConverter(_jsonSerializer).ConvertSourceToIntermediate(null!, null!, input, false);
+ if (expected is null)
+ {
+ Assert.IsNull(result);
+ return;
+ }
+
+ Assert.IsNotNull(result);
+ Assert.IsInstanceOf(result);
+ var dateTime = (DateTime2ValueConverter.DateTime2)result;
+ Assert.AreEqual(expected.Date, dateTime.Date);
+ Assert.AreEqual(expected.TimeZone, dateTime.TimeZone);
+ }
+
+ private static readonly DateTime2ValueConverter.DateTime2 _convertToObjectInputDate = new()
+ {
+ Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-1)),
+ TimeZone = "Europe/Copenhagen",
+ };
+
+ private static object[] _convertToObjectCases =
+ [
+ new object[] { null, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.All, null },
+ new object[] { _convertToObjectInputDate, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.All, DateTimeOffset.Parse("2025-08-20T16:30:00-01:00") },
+ new object[] { _convertToObjectInputDate, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.None, DateTime.Parse("2025-08-20T17:30:00") },
+ new object[] { _convertToObjectInputDate, DateTime2Configuration.DateTimeFormat.TimeOnly, DateTime2Configuration.TimeZoneMode.None, TimeOnly.Parse("17:30:00") },
+ new object[] { _convertToObjectInputDate, DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.None, DateOnly.Parse("2025-08-20") },
+ ];
+
+ [TestCaseSource(nameof(_convertToObjectCases))]
+ public void Can_Convert_To_Object(
+ DateTime2ValueConverter.DateTime2? input,
+ DateTime2Configuration.DateTimeFormat format,
+ DateTime2Configuration.TimeZoneMode timeZoneMode,
+ object? expected)
+ {
+ var dataType = new PublishedDataType(
+ 0,
+ "test",
+ "test",
+ new Lazy(() =>
+ new DateTime2Configuration
+ {
+ Format = format,
+ TimeZones = new DateTime2Configuration.TimeZonesConfiguration { Mode = timeZoneMode },
+ }));
+
+ var propertyType = new Mock(MockBehavior.Strict);
+ propertyType.SetupGet(x => x.DataType)
+ .Returns(dataType);
+
+ var result = new DateTime2ValueConverter(_jsonSerializer)
+ .ConvertIntermediateToObject(null!, propertyType.Object, PropertyCacheLevel.Unknown, input, false);
+ if (expected is null)
+ {
+ Assert.IsNull(result);
+ return;
+ }
+
+ Assert.IsNotNull(result);
+ Assert.AreEqual(expected, result);
+ }
+}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateWithTimeZoneValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateWithTimeZoneValueConverterTests.cs
deleted file mode 100644
index 5d105aaa0964..000000000000
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateWithTimeZoneValueConverterTests.cs
+++ /dev/null
@@ -1,132 +0,0 @@
-using Moq;
-using NUnit.Framework;
-using Umbraco.Cms.Core;
-using Umbraco.Cms.Core.Models.PublishedContent;
-using Umbraco.Cms.Core.PropertyEditors;
-using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
-using Umbraco.Cms.Core.Serialization;
-using Umbraco.Cms.Infrastructure.Serialization;
-
-namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors.ValueConverters;
-
-[TestFixture]
-public class DateWithTimeZoneValueConverterTests
-{
- private readonly IJsonSerializer _jsonSerializer =
- new SystemTextJsonSerializer(new DefaultJsonSerializerEncoderFactory());
-
- [TestCase(Constants.PropertyEditors.Aliases.DateTimeWithTimeZone, true)]
- [TestCase(Constants.PropertyEditors.Aliases.DateTime, false)]
- public void IsConverter_For(string propertyEditorAlias, bool expected)
- {
- var propertyType = Mock.Of(x => x.EditorAlias == propertyEditorAlias);
- var converter = new DateTimeWithTimeZoneValueConverter(Mock.Of(MockBehavior.Strict));
-
- var result = converter.IsConverter(propertyType);
-
- Assert.AreEqual(expected, result);
- }
-
- [TestCase(DateTimeWithTimeZoneFormat.DateOnly, DateTimeWithTimeZoneMode.None, typeof(DateOnly?))]
- [TestCase(DateTimeWithTimeZoneFormat.DateOnly, DateTimeWithTimeZoneMode.All, typeof(DateOnly?))]
- [TestCase(DateTimeWithTimeZoneFormat.DateOnly, DateTimeWithTimeZoneMode.Custom, typeof(DateOnly?))]
- [TestCase(DateTimeWithTimeZoneFormat.DateOnly, DateTimeWithTimeZoneMode.Local, typeof(DateOnly?))]
- [TestCase(DateTimeWithTimeZoneFormat.TimeOnly, DateTimeWithTimeZoneMode.None, typeof(TimeOnly?))]
- [TestCase(DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.None, typeof(DateTime?))]
- [TestCase(DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.All, typeof(DateTimeOffset?))]
- [TestCase(DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.Custom, typeof(DateTimeOffset?))]
- [TestCase(DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.Local, typeof(DateTimeOffset?))]
- public void GetPropertyValueType_ReturnsExpectedType(DateTimeWithTimeZoneFormat format, DateTimeWithTimeZoneMode timeZoneMode, Type expectedType)
- {
- var converter = new DateTimeWithTimeZoneValueConverter(Mock.Of(MockBehavior.Strict));
- var dataType = new PublishedDataType(
- 0,
- "test",
- "test",
- new Lazy(() =>
- new DateTimeWithTimeZoneConfiguration
- {
- Format = format,
- TimeZones = new DateTimeWithTimeZoneTimeZones { Mode = timeZoneMode },
- }));
- var propertyType = Mock.Of(x => x.DataType == dataType);
-
- var result = converter.GetPropertyValueType(propertyType);
-
- Assert.AreEqual(expectedType, result);
- }
-
- private static object[] _convertToIntermediateCases =
- [
- new object[] { null, null },
- new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":null}", new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = null } },
- new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":\"Europe/Copenhagen\"}", new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = "Europe/Copenhagen" } },
- new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":\"Europe/Copenhagen\"}", new DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" } },
- ];
-
- [TestCaseSource(nameof(_convertToIntermediateCases))]
- public void Can_Convert_To_Intermediate_Value(string? input, DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone? expected)
- {
- var result = new DateTimeWithTimeZoneValueConverter(_jsonSerializer).ConvertSourceToIntermediate(null!, null!, input, false);
- if (expected is null)
- {
- Assert.IsNull(result);
- return;
- }
-
- Assert.IsNotNull(result);
- Assert.IsInstanceOf(result);
- var dateWithTimeZone = (DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone)result;
- Assert.AreEqual(expected.Date, dateWithTimeZone.Date);
- Assert.AreEqual(expected.TimeZone, dateWithTimeZone.TimeZone);
- }
-
- private static readonly DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone _convertToObjectInputDate = new()
- {
- Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-1)),
- TimeZone = "Europe/Copenhagen",
- };
-
- private static object[] _convertToObjectCases =
- [
- new object[] { null, DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.All, null },
- new object[] { _convertToObjectInputDate, DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.All, DateTimeOffset.Parse("2025-08-20T16:30:00-01:00") },
- new object[] { _convertToObjectInputDate, DateTimeWithTimeZoneFormat.DateTime, DateTimeWithTimeZoneMode.None, DateTime.Parse("2025-08-20T17:30:00") },
- new object[] { _convertToObjectInputDate, DateTimeWithTimeZoneFormat.TimeOnly, DateTimeWithTimeZoneMode.None, TimeOnly.Parse("17:30:00") },
- new object[] { _convertToObjectInputDate, DateTimeWithTimeZoneFormat.DateOnly, DateTimeWithTimeZoneMode.None, DateOnly.Parse("2025-08-20") },
- ];
-
- [TestCaseSource(nameof(_convertToObjectCases))]
- public void Can_Convert_To_Object(
- DateTimeWithTimeZoneValueConverter.DateTimeWithTimeZone? input,
- DateTimeWithTimeZoneFormat format,
- DateTimeWithTimeZoneMode timeZoneMode,
- object? expected)
- {
- var dataType = new PublishedDataType(
- 0,
- "test",
- "test",
- new Lazy(() =>
- new DateTimeWithTimeZoneConfiguration
- {
- Format = format,
- TimeZones = new DateTimeWithTimeZoneTimeZones { Mode = timeZoneMode },
- }));
-
- var propertyType = new Mock(MockBehavior.Strict);
- propertyType.SetupGet(x => x.DataType)
- .Returns(dataType);
-
- var result = new DateTimeWithTimeZoneValueConverter(_jsonSerializer)
- .ConvertIntermediateToObject(null!, propertyType.Object, PropertyCacheLevel.Unknown, input, false);
- if (expected is null)
- {
- Assert.IsNull(result);
- return;
- }
-
- Assert.IsNotNull(result);
- Assert.AreEqual(expected, result);
- }
-}
From f04122336e8f3704889702c274fad94ea4a8620d Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Tue, 26 Aug 2025 15:56:28 +0200
Subject: [PATCH 45/86] Fix data type count tests
---
.../Repositories/DataTypeDefinitionRepositoryTest.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
index f795e80cd29d..6d49d54f1773 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
@@ -250,7 +250,7 @@ public void Can_Perform_GetAll_On_DataTypeDefinitionRepository()
Assert.That(dataTypeDefinitions, Is.Not.Null);
Assert.That(dataTypeDefinitions.Any(), Is.True);
Assert.That(dataTypeDefinitions.Any(x => x == null), Is.False);
- Assert.That(dataTypeDefinitions.Length, Is.EqualTo(35));
+ Assert.That(dataTypeDefinitions.Length, Is.EqualTo(36));
}
}
@@ -297,7 +297,7 @@ public void Can_Perform_Count_On_DataTypeDefinitionRepository()
var count = DataTypeRepository.Count(query);
// Assert
- Assert.That(count, Is.EqualTo(5));
+ Assert.That(count, Is.EqualTo(6));
}
}
From 36876a5d7aab6ef65acc6d0e8246c3e0c33bd960 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Wed, 27 Aug 2025 12:26:41 +0200
Subject: [PATCH 46/86] Simplifying code + adjusting value converter to support
old picker value
---
.../DateTime2ConfigurationEditor.cs | 0
.../DateTime2PropertyEditor.cs | 23 ++--
.../DateTime2PropertyIndexValueFactory.cs | 5 +-
.../DateTime2ValueConverter.cs | 110 +++++++++---------
4 files changed, 67 insertions(+), 71 deletions(-)
rename src/{Umbraco.Core => Umbraco.Infrastructure}/PropertyEditors/DateTime2ConfigurationEditor.cs (100%)
rename src/{Umbraco.Core => Umbraco.Infrastructure}/PropertyEditors/DateTime2PropertyEditor.cs (90%)
rename src/{Umbraco.Core => Umbraco.Infrastructure}/PropertyEditors/DateTime2PropertyIndexValueFactory.cs (90%)
rename src/{Umbraco.Core => Umbraco.Infrastructure}/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs (52%)
diff --git a/src/Umbraco.Core/PropertyEditors/DateTime2ConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2ConfigurationEditor.cs
similarity index 100%
rename from src/Umbraco.Core/PropertyEditors/DateTime2ConfigurationEditor.cs
rename to src/Umbraco.Infrastructure/PropertyEditors/DateTime2ConfigurationEditor.cs
diff --git a/src/Umbraco.Core/PropertyEditors/DateTime2PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
similarity index 90%
rename from src/Umbraco.Core/PropertyEditors/DateTime2PropertyEditor.cs
rename to src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
index 76f373b22338..71894086ffa2 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTime2PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
@@ -19,6 +19,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataEditor(
Constants.PropertyEditors.Aliases.DateTime2,
+ ValueType = ValueTypes.Json,
ValueEditorIsReusable = true)]
public class DateTime2PropertyEditor : DataEditor
{
@@ -66,6 +67,7 @@ public DateTime2DataValueEditor(
Validators.AddRange(new DateTimeWithTimeZoneValidator(localizedTextService));
}
+ ///
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
if (editorValue.Value is not JsonObject valueAsJsonObject)
@@ -102,30 +104,29 @@ public DateTime2DataValueEditor(
return jsonStr;
}
+ ///
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var value = property.GetValue(culture, segment);
- if (value is not string valueString)
- {
- return null;
- }
- var interValue = DateTime2ValueConverter.GetIntermediateValue(valueString, _jsonSerializer);
- if (interValue is not DateTime2ValueConverter.DateTime2 dateTime)
+ var interValue = DateTime2ValueConverter.GetIntermediateFromSource(value, _jsonSerializer);
+ if (interValue is not DateTime2ValueConverter.DateTime2 dateTime2)
{
return null;
}
- DateTime2Configuration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
- var objectValue = DateTime2ValueConverter.GetObjectValue(dateTime, configuration);
+ DateTime2Configuration? configuration = GetConfiguration(property.PropertyType.DataTypeKey);
+ var objectValue = DateTime2ValueConverter.GetObjectFromIntermediate(dateTime2, configuration);
JsonNode node = new JsonObject();
- node["date"] = DateTime2ValueConverter.GetDateValueAsString(objectValue);
- node["timeZone"] = dateTime.TimeZone;
-
+ node["date"] = objectValue is null ? null : $"{objectValue:O}";
+ node["timeZone"] = dateTime2.TimeZone;
return node;
}
+ private DateTime2Configuration? GetConfiguration(Guid dataTypeKey) =>
+ _dataTypeConfigurationCache.GetConfigurationAs(dataTypeKey);
+
///
/// Validates the color selection for the color picker property editor.
///
diff --git a/src/Umbraco.Core/PropertyEditors/DateTime2PropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyIndexValueFactory.cs
similarity index 90%
rename from src/Umbraco.Core/PropertyEditors/DateTime2PropertyIndexValueFactory.cs
rename to src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyIndexValueFactory.cs
index 101799b545e0..1667e9e05ec0 100644
--- a/src/Umbraco.Core/PropertyEditors/DateTime2PropertyIndexValueFactory.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyIndexValueFactory.cs
@@ -36,14 +36,13 @@ public IEnumerable GetIndexValues(
};
var propertyValue = property.GetValue(culture, segment, published);
- var sourceStr = propertyValue?.ToString();
- if (sourceStr is null)
+ if (propertyValue is null)
{
return [indexValue];
}
DateTime2Configuration? configuration = _dataTypeConfigurationCache.GetConfigurationAs(property.PropertyType.DataTypeKey);
- var value = DateTime2ValueConverter.GetValue(sourceStr, configuration, _jsonSerializer);
+ var value = DateTime2ValueConverter.GetObjectFromSource(propertyValue, configuration, _jsonSerializer);
if (value is null)
{
return [indexValue];
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
similarity index 52%
rename from src/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
rename to src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
index 5efe40921e1a..287856476dd1 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
///
/// The date time with timezone property value converter.
///
-[DefaultPropertyValueConverter]
+[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
public class DateTime2ValueConverter : PropertyValueConverterBase
{
private readonly IJsonSerializer _jsonSerializer;
@@ -42,16 +42,9 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
IPublishedPropertyType propertyType,
object? source,
bool preview)
- {
- var sourceStr = source?.ToString();
- if (sourceStr is null || !_jsonSerializer.TryDeserialize(sourceStr, out DateTime2? dateTime))
- {
- return null;
- }
-
- return dateTime;
- }
+ => GetIntermediateFromSource(source, _jsonSerializer);
+ ///
public override object? ConvertIntermediateToObject(
IPublishedElement owner,
IPublishedPropertyType propertyType,
@@ -61,71 +54,74 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
{
DateTime2Configuration? config =
ConfigurationEditor.ConfigurationAs(propertyType.DataType.ConfigurationObject);
- return GetObjectValue(inter, config);
+ return GetObjectFromIntermediate(inter, config);
}
- private static Type GetPropertyValueType(DateTime2Configuration? config) =>
- config?.Format switch
+ ///
+ /// Converts the source value to an intermediate representation.
+ ///
+ /// The source value.
+ /// The JSON serializer.
+ /// The intermediate representation.
+ internal static DateTime2? GetIntermediateFromSource(object? source, IJsonSerializer jsonSerializer) =>
+ source switch
{
- DateTime2Configuration.DateTimeFormat.DateOnly => typeof(DateOnly?),
- DateTime2Configuration.DateTimeFormat.TimeOnly => typeof(TimeOnly?),
- DateTime2Configuration.DateTimeFormat.DateTime when config.TimeZones?.Mode is not { } mode || mode == DateTime2Configuration.TimeZoneMode.None => typeof(DateTime?),
- _ => typeof(DateTimeOffset?),
+ // This DateTime check is for compatibility with the "deprecated" `Umbraco.DateTime`.
+ // Once that is removed, this can be removed too.
+ DateTime dateTime => new DateTime2 { Date = new DateTimeOffset(dateTime, TimeSpan.Zero) },
+ string sourceStr => jsonSerializer.TryDeserialize(sourceStr, out DateTime2? dateTime2) ? dateTime2 : null,
+ _ => null,
};
- internal static object? GetIntermediateValue(string? source, IJsonSerializer jsonSerializer)
- {
- if (source is null || !jsonSerializer.TryDeserialize(source, out DateTime2? dateTime))
- {
- return null;
- }
-
- return dateTime;
- }
-
- internal static object? GetObjectValue(object? inter, DateTime2Configuration? configuration)
+ ///
+ /// Converts the intermediate value to the appropriate type based on the configuration.
+ ///
+ /// The intermediate value.
+ /// The configuration.
+ /// The converted value.
+ internal static object? GetObjectFromIntermediate(object? inter, DateTime2Configuration? configuration)
{
Type propertyValueType = GetPropertyValueType(configuration);
- if (inter is null)
+ if (inter is not DateTime2 dateTime2)
{
return propertyValueType.GetDefaultValue();
}
- if (inter is not DateTime2 dateTime)
+ return propertyValueType switch
{
- return propertyValueType.GetDefaultValue();
- }
-
- if (propertyValueType == typeof(DateOnly?))
- {
- return DateOnly.FromDateTime(dateTime.Date.UtcDateTime);
- }
-
- if (propertyValueType == typeof(TimeOnly?))
- {
- return TimeOnly.FromDateTime(dateTime.Date.UtcDateTime);
- }
-
- if (propertyValueType == typeof(DateTime?))
- {
- return DateTime.SpecifyKind(dateTime.Date.UtcDateTime, DateTimeKind.Unspecified);
- }
-
- return dateTime.Date;
+ _ when propertyValueType == typeof(DateOnly?)
+ => DateOnly.FromDateTime(dateTime2.Date.UtcDateTime),
+ _ when propertyValueType == typeof(TimeOnly?)
+ => TimeOnly.FromDateTime(dateTime2.Date.UtcDateTime),
+ _ when propertyValueType == typeof(DateTime?)
+ => DateTime.SpecifyKind(dateTime2.Date.UtcDateTime, DateTimeKind.Unspecified),
+ _ => dateTime2.Date,
+ };
}
- internal static object? GetValue(string? source, DateTime2Configuration? configuration, IJsonSerializer jsonSerializer)
+ ///
+ /// Gets the object value from the source based on the configuration.
+ ///
+ /// The source value.
+ /// The configuration.
+ /// The JSON serializer.
+ /// The object value.
+ internal static object? GetObjectFromSource(
+ object? source,
+ DateTime2Configuration? configuration,
+ IJsonSerializer jsonSerializer)
{
- var intermediateValue = GetIntermediateValue(source, jsonSerializer);
- return GetObjectValue(intermediateValue, configuration);
+ var intermediateValue = GetIntermediateFromSource(source, jsonSerializer);
+ return GetObjectFromIntermediate(intermediateValue, configuration);
}
- internal static string? GetDateValueAsString(object? value) =>
- value switch
+ private static Type GetPropertyValueType(DateTime2Configuration? config) =>
+ config?.Format switch
{
- DateTimeOffset or DateOnly or TimeOnly or DateTime => $"{value:O}",
- null => null,
- _ => throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported type: {value.GetType().FullName}"),
+ DateTime2Configuration.DateTimeFormat.DateOnly => typeof(DateOnly?),
+ DateTime2Configuration.DateTimeFormat.TimeOnly => typeof(TimeOnly?),
+ DateTime2Configuration.DateTimeFormat.DateTime when config.TimeZones?.Mode is not { } mode || mode == DateTime2Configuration.TimeZoneMode.None => typeof(DateTime?),
+ _ => typeof(DateTimeOffset?),
};
public class DateTime2
From 7eabf4d074fd436666e48ed6ad892dbaa4857771 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Wed, 27 Aug 2025 19:40:10 +0200
Subject: [PATCH 47/86] Adjustments to property editor unit tests
---
.../DateTime2PropertyEditor.cs | 8 +--
.../DateTime2PropertyEditorTests.cs | 49 +++++++++++++------
.../DateTime2ValueConverterTests.cs | 6 +--
3 files changed, 41 insertions(+), 22 deletions(-)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
index 71894086ffa2..7dd178538bbb 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
@@ -109,18 +109,18 @@ public DateTime2DataValueEditor(
{
var value = property.GetValue(culture, segment);
- var interValue = DateTime2ValueConverter.GetIntermediateFromSource(value, _jsonSerializer);
- if (interValue is not DateTime2ValueConverter.DateTime2 dateTime2)
+ DateTime2ValueConverter.DateTime2? interValue = DateTime2ValueConverter.GetIntermediateFromSource(value, _jsonSerializer);
+ if (interValue is null)
{
return null;
}
DateTime2Configuration? configuration = GetConfiguration(property.PropertyType.DataTypeKey);
- var objectValue = DateTime2ValueConverter.GetObjectFromIntermediate(dateTime2, configuration);
+ var objectValue = DateTime2ValueConverter.GetObjectFromIntermediate(interValue, configuration);
JsonNode node = new JsonObject();
node["date"] = objectValue is null ? null : $"{objectValue:O}";
- node["timeZone"] = dateTime2.TimeZone;
+ node["timeZone"] = interValue.TimeZone;
return node;
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
index a7123e06c4db..dec2fc346c67 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
@@ -1,6 +1,6 @@
using System.Globalization;
+using System.Text.Json;
using System.Text.Json.Nodes;
-using Json.More;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
@@ -134,23 +134,39 @@ public void Can_Parse_Values_From_Editor(
private static readonly object[][] _sourceList4 =
[
- [null, DateTime2Configuration.DateTimeFormat.DateOnly, null],
- [null, DateTime2Configuration.DateTimeFormat.TimeOnly, null],
- [null, DateTime2Configuration.DateTimeFormat.DateTime, null],
- [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTime2Configuration.DateTimeFormat.DateOnly, "{\"date\":\"2025-08-20\",\"timeZone\":null}"],
- [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTime2Configuration.DateTimeFormat.TimeOnly, "{\"date\":\"16:30:00.0000000\",\"timeZone\":null}"],
- [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.Zero) }, DateTime2Configuration.DateTimeFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":null}"],
- [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.FromHours(-5)) }, DateTime2Configuration.DateTimeFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":null}"],
- [new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" }, DateTime2Configuration.DateTimeFormat.DateTime, "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":\"Europe/Copenhagen\"}"],
+ [null, null, DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.None, null],
+ [null, null, DateTime2Configuration.DateTimeFormat.TimeOnly, DateTime2Configuration.TimeZoneMode.None, null],
+ [null, null, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.None, null],
+ [0, null, DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.None, new { date = "2025-08-20", timeZone = (string?)null }],
+ [0, null, DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.All, new { date = "2025-08-20", timeZone = (string?)null }],
+ [0, null, DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.Local, new { date = "2025-08-20", timeZone = (string?)null }],
+ [0, null, DateTime2Configuration.DateTimeFormat.DateOnly, DateTime2Configuration.TimeZoneMode.Custom, new { date = "2025-08-20", timeZone = (string?)null }],
+ [0, null, DateTime2Configuration.DateTimeFormat.TimeOnly, DateTime2Configuration.TimeZoneMode.None, new { date = "16:30:00.0000000", timeZone = (string?)null }],
+ [0, null, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.None, new { date = "2025-08-20T16:30:00.0000000", timeZone = (string?)null }],
+ [0, null, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.All, new { date = "2025-08-20T16:30:00.0000000+00:00", timeZone = (string?)null }],
+ [0, null, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.Local, new { date = "2025-08-20T16:30:00.0000000+00:00", timeZone = (string?)null }],
+ [0, null, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.Custom, new { date = "2025-08-20T16:30:00.0000000+00:00", timeZone = (string?)null }],
+ [-5, null, DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.None, new { date = "2025-08-20T21:30:00.0000000", timeZone = (string?)null }],
+ [-5, "Europe/Copenhagen", DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.None, new { date = "2025-08-20T21:30:00.0000000", timeZone = "Europe/Copenhagen" }],
+ [-5, "Europe/Copenhagen", DateTime2Configuration.DateTimeFormat.DateTime, DateTime2Configuration.TimeZoneMode.All, new { date = "2025-08-20T16:30:00.0000000-05:00", timeZone = "Europe/Copenhagen" }],
];
[TestCaseSource(nameof(_sourceList4))]
public void Can_Parse_Values_To_Editor(
- DateTime2ValueConverter.DateTime2? storedValue,
+ int? offset,
+ string? timeZone,
DateTime2Configuration.DateTimeFormat format,
- string? expectedJson)
+ DateTime2Configuration.TimeZoneMode timeZoneMode,
+ object? expectedResult)
{
- var valueEditor = CreateValueEditor(format: format);
+ var storedValue = offset is null
+ ? null
+ : new DateTime2ValueConverter.DateTime2
+ {
+ Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.FromHours(offset.Value)),
+ TimeZone = timeZone,
+ };
+ var valueEditor = CreateValueEditor(format: format, timeZoneMode: timeZoneMode);
_dataTypeConfigurationCache.Setup(dc => dc.GetConfigurationAs(Guid.Empty))
.Returns(valueEditor.ConfigurationObject as DateTime2Configuration);
var storedValueJson = storedValue is null ? null : _jsonSerializer.Serialize(storedValue);
@@ -166,14 +182,17 @@ public void Can_Parse_Values_To_Editor(
}
],
});
- if (expectedJson is null)
+
+ if (expectedResult is null)
{
Assert.IsNull(result);
return;
}
- Assert.IsInstanceOf(result);
- Assert.AreEqual(expectedJson, (result as JsonNode)?.AsJsonString());
+ var test = JsonSerializer.SerializeToNode(expectedResult);
+
+ Assert.IsNotNull(result);
+ Assert.IsTrue(JsonNode.DeepEquals(JsonSerializer.SerializeToNode(expectedResult), result as JsonNode));
}
private DateTime2PropertyEditor.DateTime2DataValueEditor CreateValueEditor(
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
index ec7e97904ddf..a261871ec22e 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
@@ -59,9 +59,9 @@ public void GetPropertyValueType_ReturnsExpectedType(DateTime2Configuration.Date
private static object[] _convertToIntermediateCases =
[
new object[] { null, null },
- new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":null}", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = null } },
- new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000Z\",\"timeZone\":\"Europe/Copenhagen\"}", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = "Europe/Copenhagen" } },
- new object[] { "{\"date\":\"2025-08-20T16:30:00.0000000-05:00\",\"timeZone\":\"Europe/Copenhagen\"}", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" } },
+ new object[] { """{"date":"2025-08-20T16:30:00.0000000Z","timeZone":null}""", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = null } },
+ new object[] { """{"date":"2025-08-20T16:30:00.0000000Z","timeZone":"Europe/Copenhagen"}""", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = "Europe/Copenhagen" } },
+ new object[] { """{"date":"2025-08-20T16:30:00.0000000-05:00","timeZone":"Europe/Copenhagen"}""", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" } },
];
[TestCaseSource(nameof(_convertToIntermediateCases))]
From 4e6d33e00207cb2ff2a3d82b1a150e4a01655c01 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Mon, 1 Sep 2025 16:24:42 +0200
Subject: [PATCH 48/86] Fix validation issue
---
.../src/assets/lang/en.ts | 3 +-
.../src/assets/lang/pt.ts | 3 +-
.../date-time-picker/Umbraco.DateTime2.ts | 14 ++---
...erty-editor-ui-date-time-picker.element.ts | 59 +++++++++++--------
...erty-editor-ui-time-zone-picker.element.ts | 8 +--
5 files changed, 50 insertions(+), 37 deletions(-)
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
index 07fc6832a8ce..fa78395203bf 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
@@ -2826,7 +2826,7 @@ export default {
resetUrlMessage: 'Are you sure you want to reset this URL?',
resetUrlLabel: 'Reset',
},
- timeZonePicker: {
+ dateTimePicker: {
local: 'Local',
differentTimeZoneLabel: (offset: string, localDate: string) =>
`The selected time (${offset}) is equivalent to ${localDate} in your local time.`,
@@ -2842,6 +2842,7 @@ export default {
config_timeZones_all: 'All - Display all available time zones',
config_timeZones_local: 'Local - Display only the local time zone',
config_timeZones_custom: 'Custom - Display a pre-defined list of time zones',
+ emptyDate: 'Please select a date',
emptyTimeZone: 'Please select a time zone',
invalidTimeZone: 'The selected time zone is not valid',
},
diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts
index 4198db2f507b..0aeaace8822b 100644
--- a/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts
+++ b/src/Umbraco.Web.UI.Client/src/assets/lang/pt.ts
@@ -2830,7 +2830,7 @@ export default {
resetUrlMessage: 'Tem a certeza que quer redefinir este URL?',
resetUrlLabel: 'Redefinir',
},
- timeZonePicker: {
+ dateTimePicker: {
local: 'Local',
differentTimeZoneLabel: (offset: string, localDate: string) =>
`A hora selecionada (${offset}) é equivalente a ${localDate} no fuso horário local.`,
@@ -2846,6 +2846,7 @@ export default {
config_timeZones_all: 'Todos - Mostrar todos os fusos horários disponÃveis',
config_timeZones_local: 'Local - Mostrar apenas o fuso horário local',
config_timeZones_custom: 'Personalizado - Mostrar uma lista pré-definida de fusos horários',
+ emptyDate: 'Por favor, selecione uma data',
emptyTimeZone: 'Por favor, selecione um fuso horário',
invalidTimeZone: 'O fuso horário selecionado não é válido',
},
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/Umbraco.DateTime2.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/Umbraco.DateTime2.ts
index 18d65ea68992..2fe49b84d581 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/Umbraco.DateTime2.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/Umbraco.DateTime2.ts
@@ -10,22 +10,22 @@ export const manifest: ManifestPropertyEditorSchema = {
properties: [
{
alias: 'format',
- label: '#timeZonePicker_config_format',
+ label: '#dateTimePicker_config_format',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.RadioButtonList',
config: [
{
alias: 'items',
value: [
- { name: '#timeZonePicker_config_format_datetime', value: 'date-time' },
- { name: '#timeZonePicker_config_format_dateOnly', value: 'date-only' },
- { name: '#timeZonePicker_config_format_timeOnly', value: 'time-only' },
+ { name: '#dateTimePicker_config_format_datetime', value: 'date-time' },
+ { name: '#dateTimePicker_config_format_dateOnly', value: 'date-only' },
+ { name: '#dateTimePicker_config_format_timeOnly', value: 'time-only' },
],
},
],
},
{
alias: 'timeFormat',
- label: '#timeZonePicker_config_timeFormat',
+ label: '#dateTimePicker_config_timeFormat',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.RadioButtonList',
config: [
{
@@ -39,8 +39,8 @@ export const manifest: ManifestPropertyEditorSchema = {
},
{
alias: 'timeZones',
- label: '#timeZonePicker_config_timeZones',
- description: '{#timeZonePicker_config_timeZones_description}',
+ label: '#dateTimePicker_config_timeZones',
+ description: '{#dateTimePicker_config_timeZones_description}',
propertyEditorUiAlias: 'Umb.PropertyEditorUi.TimeZonePicker',
config: [],
},
diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/property-editor-ui-date-time-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/property-editor-ui-date-time-picker.element.ts
index 065f92a39129..29ea848b5f62 100644
--- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/property-editor-ui-date-time-picker.element.ts
+++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/date-time-picker/property-editor-ui-date-time-picker.element.ts
@@ -41,7 +41,7 @@ interface UmbTimeZonePickerOption extends UmbTimeZone {
* @element umb-property-editor-ui-date-time-picker
*/
@customElement('umb-property-editor-ui-date-time-picker')
-export class UmbPropertyEditorUIDateWithTimeZonePickerElement
+export class UmbPropertyEditorUIDateTimePickerElement
extends UmbFormControlMixin(UmbLitElement)
implements UmbPropertyEditorUiElement
{
@@ -86,17 +86,27 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
this.addValidator(
'customError',
- () => this.localize.term('timeZonePicker_emptyTimeZone'),
+ () => this.localize.term('dateTimePicker_emptyDate'),
() => {
return (
- (!!this.mandatory && !this.value?.date) || (this._displayTimeZone && !!(this.value && !this.value.timeZone))
+ !!this.mandatory && !this.value?.date
);
},
);
this.addValidator(
'customError',
- () => this.localize.term('timeZonePicker_invalidTimeZone'),
+ () => this.localize.term('dateTimePicker_emptyTimeZone'),
+ () => {
+ return (
+ !!(this.value?.date) && this._displayTimeZone && !this.value.timeZone
+ );
+ },
+ );
+
+ this.addValidator(
+ 'customError',
+ () => this.localize.term('dateTimePicker_invalidTimeZone'),
() => {
return (
this._displayTimeZone &&
@@ -112,35 +122,38 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
const dateFormat = config.getValueByAlias('format');
const timeFormat = config.getValueByAlias('timeFormat');
- let includeTimeZones = false;
+ let timeZonePickerConfig : UmbTimeZonePickerValue | undefined = undefined;
switch (dateFormat) {
case 'date-only':
this._dateInputType = 'date';
this._dateInputFormat = 'yyyy-MM-dd';
+ this._displayTimeZone = false;
break;
case 'time-only':
this._dateInputType = 'time';
this._dateInputFormat = 'HH:mm:ss';
this.#setTimeInputStep(timeFormat);
+ this._displayTimeZone = false;
break;
default:
this._dateInputType = 'datetime-local';
this.#setTimeInputStep(timeFormat);
- includeTimeZones = true;
+ timeZonePickerConfig = config.getValueByAlias('timeZones');
+ this._displayTimeZone = !!timeZonePickerConfig;
break;
}
- this.#prefillValue(config, includeTimeZones);
+ this.#prefillValue(timeZonePickerConfig);
}
- #prefillValue(config: UmbPropertyEditorConfigCollection, includeTimeZones: boolean) {
+ #prefillValue(timeZonePickerConfig: UmbTimeZonePickerValue | undefined) {
const date = this.value?.date;
const zone = this.value?.timeZone;
if (!date) {
- if (includeTimeZones) {
+ if (timeZonePickerConfig) {
// If the date is not provided, we prefill the time zones using the current date (used to retrieve the offsets)
- this.#prefillTimeZones(config, DateTime.now());
+ this.#prefillTimeZones(timeZonePickerConfig, DateTime.now());
}
return;
}
@@ -148,30 +161,29 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
// Load the date from the value
const dateTime = DateTime.fromISO(date, { zone: zone ?? 'UTC' }); // If no zone is provided, we default to UTC.
if (!dateTime.isValid) {
- if (includeTimeZones) {
+ if (timeZonePickerConfig) {
// If the date is invalid, we prefill the time zones using the current date (used to retrieve the offsets)
- this.#prefillTimeZones(config, DateTime.now());
+ this.#prefillTimeZones(timeZonePickerConfig, DateTime.now());
}
- console.warn(`[UmbDateWithTimeZonePicker] Invalid date format: ${date}`);
+ console.warn(`[UmbPropertyEditorUIDateTimePickerElement] Invalid date format: ${date}`);
return;
}
this._selectedDate = dateTime;
this._datePickerValue = dateTime.toFormat(this._dateInputFormat);
- if (includeTimeZones) {
- this.#prefillTimeZones(config, dateTime);
+ if (timeZonePickerConfig) {
+ this.#prefillTimeZones(timeZonePickerConfig, dateTime);
}
}
- #prefillTimeZones(config: UmbPropertyEditorConfigCollection, selectedDate: DateTime | undefined) {
+ #prefillTimeZones(config: UmbTimeZonePickerValue | undefined, selectedDate: DateTime | undefined) {
// Retrieve the time zones from the config
- const timeZonePickerConfig = config.getValueByAlias('timeZones');
this._clientTimeZone = getClientTimeZone();
// Retrieve the time zones from the config
const dateToCalculateOffset = selectedDate ?? DateTime.now();
- switch (timeZonePickerConfig?.mode) {
+ switch (config?.mode) {
case 'all':
this._timeZoneOptions = this._filteredTimeZoneOptions = getTimeZoneList(undefined).map((tz) => ({
...tz,
@@ -188,7 +200,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
break;
}
case 'custom': {
- this._timeZoneOptions = this._filteredTimeZoneOptions = getTimeZoneList(timeZonePickerConfig.timeZones).map(
+ this._timeZoneOptions = this._filteredTimeZoneOptions = getTimeZoneList(config.timeZones).map(
(tz) => ({
...tz,
offset: getTimeZoneOffset(tz.value, dateToCalculateOffset),
@@ -212,7 +224,6 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
break;
}
default:
- this._displayTimeZone = false;
return;
}
@@ -390,7 +401,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
if (this._timeZoneOptions.length === 1) {
return html`${this._timeZoneOptions[0].name} ${this._timeZoneOptions[0].value ===
this._clientTimeZone?.value
- ? ` (${this.localize.term('timeZonePicker_local')})`
+ ? ` (${this.localize.term('dateTimePicker_local')})`
: nothing}`;
}
@@ -427,7 +438,7 @@ export class UmbPropertyEditorUIDateWithTimeZonePickerElement
return html` ${this.localize.term(
- 'timeZonePicker_differentTimeZoneLabel',
+ 'dateTimePicker_differentTimeZoneLabel',
`UTC${this._selectedDate.toFormat('Z')}`,
this._selectedDate.toLocal().toFormat('ff'),
)}
From 25ece9385925e4c5e588a6239f93750292dfe50e Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Mon, 1 Sep 2025 16:40:49 +0200
Subject: [PATCH 49/86] Fix default configuration for 'Date Time (Unspecified)'
---
.../Migrations/Install/DatabaseDataCreator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index 3e0e3de05ffd..ce6ab60fb386 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -2343,7 +2343,7 @@ string GridCollectionView(string collectionViewType) =>
EditorAlias = Constants.PropertyEditors.Aliases.DateTime2,
EditorUiAlias = "Umb.PropertyEditorUi.DateTimePicker",
DbType = "Ntext",
- Configuration = "{\"format\": \"date-time\", \"timeFormat\": \"HH:mm\", \"timeZones\": {\"mode\": \"none\"}}",
+ Configuration = "{\"format\": \"date-time\", \"timeFormat\": \"HH:mm\", \"timeZones\": null}",
});
}
From 1f03cb091e8bf6a9c82c40166e55066b98c2c3f3 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Wed, 3 Sep 2025 14:17:47 +0200
Subject: [PATCH 50/86] Rename validator
---
.../PropertyEditors/DateTime2PropertyEditor.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
index 7dd178538bbb..913055d2dd63 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
@@ -64,7 +64,7 @@ public DateTime2DataValueEditor(
{
_jsonSerializer = jsonSerializer;
_dataTypeConfigurationCache = dataTypeConfigurationCache;
- Validators.AddRange(new DateTimeWithTimeZoneValidator(localizedTextService));
+ Validators.AddRange(new DateTime2Validator(localizedTextService));
}
///
@@ -130,11 +130,11 @@ public DateTime2DataValueEditor(
///
/// Validates the color selection for the color picker property editor.
///
- private class DateTimeWithTimeZoneValidator : IValueValidator
+ private class DateTime2Validator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
- public DateTimeWithTimeZoneValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
+ public DateTime2Validator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
///
public IEnumerable
Validate(
From 2d2d4057d9ed9a5f151f9e3211a1e307059598bb Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Wed, 3 Sep 2025 14:34:54 +0200
Subject: [PATCH 51/86] Fix comment
---
.../PropertyEditors/DateTime2PropertyEditor.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
index 913055d2dd63..686af339a348 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
@@ -128,7 +128,7 @@ public DateTime2DataValueEditor(
_dataTypeConfigurationCache.GetConfigurationAs(dataTypeKey);
///
- /// Validates the color selection for the color picker property editor.
+ /// Validates the date time selection for the DateTime2 property editor.
///
private class DateTime2Validator : IValueValidator
{
From e3d54f6714b9babce815f5cdcf270c0c75d09094 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 5 Sep 2025 13:28:14 +0200
Subject: [PATCH 52/86] Adjust database creator default DateTime2 data types
---
src/Umbraco.Core/Constants-DataTypes.cs | 18 ++------
.../Migrations/Install/DatabaseDataCreator.cs | 41 ++-----------------
2 files changed, 7 insertions(+), 52 deletions(-)
diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs
index 7dcdce3707c0..e2d82864b207 100644
--- a/src/Umbraco.Core/Constants-DataTypes.cs
+++ b/src/Umbraco.Core/Constants-DataTypes.cs
@@ -116,14 +116,9 @@ public static class Guids
public const string DatePickerWithTime = "e4d66c0f-b935-4200-81f0-025f7256b89a";
///
- /// Guid for Date Time 2 (Unspecified) as string
+ /// Guid for Date Time Picker (with Timezone) as string
///
- public const string DateTime2Unspecified = "435B91BC-841B-4185-8402-5A8D462BCAF4";
-
- ///
- /// Guid for Date Time 2 With Timezone as string
- ///
- public const string DateTime2WithTimeZone = "88E8A052-30EE-4D44-A507-59F2CDFC769C";
+ public const string DateTimePickerWithTimeZone = "88E8A052-30EE-4D44-A507-59F2CDFC769C";
///
/// Guid for Approved Color as string
@@ -301,14 +296,9 @@ public static class Guids
public static readonly Guid DatePickerWithTimeGuid = new(DatePickerWithTime);
///
- /// Guid for Date Time 2 (Unspecified).
- ///
- public static readonly Guid DateTime2UnspecifiedGuid = new(DateTime2Unspecified);
-
- ///
- /// Guid for Date Time 2 (With Timezone).
+ /// Guid for Date Time Picker (with Timezone).
///
- public static readonly Guid DateTime2WithTimeZoneGuid = new(DateTime2WithTimeZone);
+ public static readonly Guid DateTimePickerWithTimeZoneGuid = new(DateTimePickerWithTimeZone);
///
/// Guid for Approved Color
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index ce6ab60fb386..52caf589f815 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -859,7 +859,7 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text)
"id");
ConditionalInsert(
Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes,
- Constants.DataTypes.Guids.DateTime2Unspecified,
+ Constants.DataTypes.Guids.DateTimePickerWithTimeZone,
new NodeDto
{
NodeId = 1055,
@@ -869,27 +869,8 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text)
Level = 1,
Path = "-1,1055",
SortOrder = 2,
- UniqueId = Constants.DataTypes.Guids.DateTime2UnspecifiedGuid,
- Text = "Date Time (Unspecified)",
- NodeObjectType = Constants.ObjectTypes.DataType,
- CreateDate = DateTime.Now,
- },
- Constants.DatabaseSchema.Tables.Node,
- "id");
- ConditionalInsert(
- Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes,
- Constants.DataTypes.Guids.DateTime2WithTimeZone,
- new NodeDto
- {
- NodeId = 1056,
- Trashed = false,
- ParentId = -1,
- UserId = -1,
- Level = 1,
- Path = "-1,1056",
- SortOrder = 2,
- UniqueId = Constants.DataTypes.Guids.DateTime2WithTimeZoneGuid,
- Text = "Date Time (With Time Zone)",
+ UniqueId = Constants.DataTypes.Guids.DateTimePickerWithTimeZoneGuid,
+ Text = "Date Time Picker (with time zone)",
NodeObjectType = Constants.ObjectTypes.DataType,
CreateDate = DateTime.Now,
},
@@ -2343,22 +2324,6 @@ string GridCollectionView(string collectionViewType) =>
EditorAlias = Constants.PropertyEditors.Aliases.DateTime2,
EditorUiAlias = "Umb.PropertyEditorUi.DateTimePicker",
DbType = "Ntext",
- Configuration = "{\"format\": \"date-time\", \"timeFormat\": \"HH:mm\", \"timeZones\": null}",
- });
- }
-
- if (_database.Exists(1056))
- {
- _database.Insert(
- Constants.DatabaseSchema.Tables.DataType,
- "pk",
- false,
- new DataTypeDto
- {
- NodeId = 1056,
- EditorAlias = Constants.PropertyEditors.Aliases.DateTime2,
- EditorUiAlias = "Umb.PropertyEditorUi.DateTimePicker",
- DbType = "Ntext",
Configuration = "{\"format\": \"date-time\", \"timeFormat\": \"HH:mm\", \"timeZones\": {\"mode\": \"all\"}}",
});
}
From 87b45751a912710de045850bbd8cf44ba7f421e1 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 5 Sep 2025 14:44:32 +0200
Subject: [PATCH 53/86] Update tests after adjusting default data types
---
tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs | 1 +
tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs | 8 ++++++++
.../Umbraco.Core/Services/ContentServiceTests.cs | 3 +++
.../Repositories/DataTypeDefinitionRepositoryTest.cs | 4 ++--
4 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs
index 2767d26ce3c4..66760be82daf 100644
--- a/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs
+++ b/tests/Umbraco.Tests.Common/Builders/ContentBuilder.cs
@@ -464,6 +464,7 @@ public static Content CreateAllTypesContent(IContentType contentType, string nam
content.SetValue("memberPicker", Udi.Create(Constants.UdiEntityType.Member, new Guid("9A50A448-59C0-4D42-8F93-4F1D55B0F47D")).ToString());
content.SetValue("multiUrlPicker", "[{\"name\":\"https://test.com\",\"url\":\"https://test.com\"}]");
content.SetValue("tags", "this,is,tags");
+ content.SetValue("dateTimeWithTimeZone", "{\"date\":\"2025-01-22T18:33:01.0000000+01:00\",\"timezone\":\"Europe/Copenhagen\"}");
return content;
}
diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs
index 54e9090ddb4e..1b3c8326f922 100644
--- a/tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs
+++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeBuilder.cs
@@ -531,6 +531,14 @@ public static ContentType CreateAllTypesContentType(string alias, string name)
.WithValueStorageType(ValueStorageType.Ntext)
.WithSortOrder(20)
.Done()
+ .AddPropertyType()
+ .WithAlias("dateTimeWithTimeZone")
+ .WithName("Date Time (with time zone)")
+ .WithDataTypeId(1055)
+ .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.DateTime2)
+ .WithValueStorageType(ValueStorageType.Ntext)
+ .WithSortOrder(21)
+ .Done()
.Done()
.Build();
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs
index 65befb218ffa..77892b2f7ed5 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs
@@ -2731,6 +2731,9 @@ public void Can_Verify_Property_Types_On_Content()
Assert.That(sut.GetValue("multiUrlPicker"),
Is.EqualTo("[{\"name\":\"https://test.com\",\"url\":\"https://test.com\"}]"));
Assert.That(sut.GetValue("tags"), Is.EqualTo("this,is,tags"));
+ Assert.That(
+ sut.GetValue("dateTimeWithTimeZone"),
+ Is.EqualTo("{\"date\":\"2025-01-22T18:33:01.0000000+01:00\",\"timezone\":\"Europe/Copenhagen\"}"));
}
[Test]
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
index 6d49d54f1773..f795e80cd29d 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs
@@ -250,7 +250,7 @@ public void Can_Perform_GetAll_On_DataTypeDefinitionRepository()
Assert.That(dataTypeDefinitions, Is.Not.Null);
Assert.That(dataTypeDefinitions.Any(), Is.True);
Assert.That(dataTypeDefinitions.Any(x => x == null), Is.False);
- Assert.That(dataTypeDefinitions.Length, Is.EqualTo(36));
+ Assert.That(dataTypeDefinitions.Length, Is.EqualTo(35));
}
}
@@ -297,7 +297,7 @@ public void Can_Perform_Count_On_DataTypeDefinitionRepository()
var count = DataTypeRepository.Count(query);
// Assert
- Assert.That(count, Is.EqualTo(6));
+ Assert.That(count, Is.EqualTo(5));
}
}
From cbed45a8d0d389771695d564c8a4d8071f48ca80 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 5 Sep 2025 17:44:51 +0200
Subject: [PATCH 54/86] Add integration test for DateTime2 returned value type
---
.../DateTime2PropertyEditorTests.cs | 140 ++++++++++++++++++
1 file changed, 140 insertions(+)
create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditorTests.cs
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditorTests.cs
new file mode 100644
index 000000000000..edbb9c4bddb9
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditorTests.cs
@@ -0,0 +1,140 @@
+using System.Text.Json.Nodes;
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Notifications;
+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;
+using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors;
+
+[TestFixture]
+[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
+public class DateTime2PropertyEditorTests : UmbracoIntegrationTest
+{
+ private IDataTypeService DataTypeService => GetRequiredService();
+
+ private IContentTypeService ContentTypeService => GetRequiredService();
+
+ private IContentEditingService ContentEditingService => GetRequiredService();
+
+ private IContentPublishingService ContentPublishingService => GetRequiredService();
+
+ private IPublishedContentCache PublishedContentCache => GetRequiredService();
+
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ }
+
+ private static readonly object[] _sourceList1 =
+ [
+ new object[] { "date-only", false, new DateOnly(2025, 1, 22) },
+ new object[] { "time-only", false, new TimeOnly(18, 33, 1) },
+ new object[] { "date-time", false, new DateTime(2025, 1, 22, 18, 33, 1) },
+ new object[] { "date-time", true, new DateTimeOffset(2025, 1, 22, 18, 33, 1, TimeSpan.Zero) },
+ ];
+
+ [TestCaseSource(nameof(_sourceList1))]
+ public async Task Returns_Correct_Type_Based_On_Configuration(
+ string configuredFormat,
+ bool timeZone,
+ object expectedValue)
+ {
+ var dataType = new DataTypeBuilder()
+ .WithId(0)
+ .WithDatabaseType(ValueStorageType.Ntext)
+ .AddEditor()
+ .WithAlias(Constants.PropertyEditors.Aliases.DateTime2)
+ .WithConfigurationEditor(
+ new DateTime2ConfigurationEditor(IOHelper)
+ {
+ DefaultConfiguration = new Dictionary
+ {
+ ["format"] = configuredFormat,
+ ["timeFormat"] = "HH:mm",
+ ["timeZones"] = timeZone ? new { mode = "all" } : null,
+ },
+ })
+ .WithDefaultConfiguration(
+ new Dictionary
+ {
+ ["format"] = configuredFormat,
+ ["timeFormat"] = "HH:mm",
+ ["timeZones"] = timeZone ? new { mode = "all" } : null,
+ })
+ .Done()
+ .Build();
+
+ var dataTypeCreateResult = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey);
+ Assert.IsTrue(dataTypeCreateResult.Success);
+
+ var contentType = new ContentTypeBuilder()
+ .WithAlias("contentType")
+ .WithName("Content Type")
+ .WithAllowAsRoot(true)
+ .AddPropertyGroup()
+ .WithAlias("content")
+ .WithName("Content")
+ .WithSupportsPublishing(true)
+ .AddPropertyType()
+ .WithAlias("dateTime")
+ .WithName("Date Time")
+ .WithDataTypeId(dataTypeCreateResult.Result.Id)
+ .Done()
+ .Done()
+ .Build();
+
+ var contentTypeCreateResult = await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
+ Assert.IsTrue(contentTypeCreateResult.Success);
+
+ var content = new ContentEditingBuilder()
+ .WithContentTypeKey(contentType.Key)
+ .AddVariant()
+ .WithName("My Content")
+ .Done()
+ .AddProperty()
+ .WithAlias("dateTime")
+ .WithValue(
+ new JsonObject
+ {
+ ["date"] = "2025-01-22T18:33:01.0000000+00:00",
+ ["timeZone"] = "Europe/Copenhagen",
+ })
+ .Done()
+ .Build();
+ var createContentResult = await ContentEditingService.CreateAsync(content, Constants.Security.SuperUserKey);
+ Assert.IsTrue(createContentResult.Success);
+ Assert.IsNotNull(createContentResult.Result.Content);
+ var dateTimeProperty = createContentResult.Result.Content.Properties["dateTime"];
+ Assert.IsNotNull(dateTimeProperty, "After content creation, the property should exist");
+ Assert.IsNotNull(dateTimeProperty.GetValue(), "After content creation, the property value should not be null");
+
+ var publishResult = await ContentPublishingService.PublishBranchAsync(
+ createContentResult.Result.Content.Key,
+ [],
+ PublishBranchFilter.IncludeUnpublished,
+ Constants.Security.SuperUserKey,
+ false);
+
+ Assert.IsTrue(publishResult.Success);
+
+ var test = ((DocumentCache)PublishedContentCache).GetAtRoot(false);
+ var publishedContent = await PublishedContentCache.GetByIdAsync(createContentResult.Result.Content.Key, false);
+ Assert.IsNotNull(publishedContent);
+
+ var value = publishedContent.GetProperty("dateTime")?.GetValue();
+ Assert.IsNotNull(value);
+ Assert.AreEqual(expectedValue, value);
+ }
+}
From b4759c9519d26b04eb5d0b856bba7b268f27e136 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 5 Sep 2025 17:51:02 +0200
Subject: [PATCH 55/86] Apply suggestions from code review
Co-authored-by: Andy Butland
---
.../PropertyEditors/DateTime2PropertyEditor.cs | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
index 686af339a348..2c0189b7188d 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
@@ -100,8 +100,7 @@ public DateTime2DataValueEditor(
TimeZone = valueAsJsonObject["timeZone"]?.GetValue(),
};
- var jsonStr = _jsonSerializer.Serialize(value);
- return jsonStr;
+ return _jsonSerializer.Serialize(value);
}
///
@@ -118,10 +117,11 @@ public DateTime2DataValueEditor(
DateTime2Configuration? configuration = GetConfiguration(property.PropertyType.DataTypeKey);
var objectValue = DateTime2ValueConverter.GetObjectFromIntermediate(interValue, configuration);
- JsonNode node = new JsonObject();
- node["date"] = objectValue is null ? null : $"{objectValue:O}";
- node["timeZone"] = interValue.TimeZone;
- return node;
+ return new JsonObject
+ {
+ ["date"] = objectValue is null ? null : $"{objectValue:O}",
+ ["timeZone"] = interValue.TimeZone
+ };
}
private DateTime2Configuration? GetConfiguration(Guid dataTypeKey) =>
@@ -149,7 +149,7 @@ public IEnumerable Validate(
}
var selectedDate = valueAsJsonObject["date"]?.GetValue();
- if (selectedDate.IsNullOrWhiteSpace() || !DateTimeOffset.TryParse(selectedDate, out DateTimeOffset _))
+ if (selectedDate.IsNullOrWhiteSpace() || DateTimeOffset.TryParse(selectedDate, out DateTimeOffset _) is false)
{
yield return new ValidationResult(
_localizedTextService.Localize("validation", "invalidDate", [selectedDate]),
From f6800e973868aa94c5dc3c8a6fb314d3cd861369 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 5 Sep 2025 18:40:54 +0200
Subject: [PATCH 56/86] Aligning DateTime2Validator with other JSON validators.
Added new model for API.
---
.../DateTime2PropertyEditor.cs | 81 ++++++++++++++-----
.../DateTime2ValueConverter.cs | 15 ++--
.../DateTime2PropertyEditorTests.cs | 4 +-
.../DateTime2ValueConverterTests.cs | 16 ++--
4 files changed, 82 insertions(+), 34 deletions(-)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
index 2c0189b7188d..36aef64bb8e9 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
@@ -4,11 +4,13 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Validation;
+using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -64,24 +66,25 @@ public DateTime2DataValueEditor(
{
_jsonSerializer = jsonSerializer;
_dataTypeConfigurationCache = dataTypeConfigurationCache;
- Validators.AddRange(new DateTime2Validator(localizedTextService));
+ var validators = new TypedJsonValidatorRunner(
+ jsonSerializer,
+ new DateTime2Validator(localizedTextService));
+
+ Validators.Add(validators);
}
///
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
- if (editorValue.Value is not JsonObject valueAsJsonObject)
+ if (editorValue.Value is null ||
+ _jsonSerializer.TryDeserialize(editorValue.Value, out DateTime2ApiModel? dateTime2ApiModel) is false)
{
- return null;
+ return base.FromEditor(editorValue, currentValue);
}
- var selectedDate = valueAsJsonObject["date"]?.GetValue();
- if (selectedDate.IsNullOrWhiteSpace())
- {
- return null;
- }
-
- if (!DateTimeOffset.TryParse(selectedDate, null, DateTimeStyles.AssumeUniversal, out DateTimeOffset dateTimeOffset))
+ var selectedDate = dateTime2ApiModel.Date;
+ if (selectedDate.IsNullOrWhiteSpace()
+ || !DateTimeOffset.TryParse(selectedDate, null, DateTimeStyles.AssumeUniversal, out DateTimeOffset dateTimeOffset))
{
return null;
}
@@ -94,10 +97,10 @@ public DateTime2DataValueEditor(
dateTimeOffset = new DateTimeOffset(DateOnly.MinValue, TimeOnly.FromTimeSpan(dateTimeOffset.TimeOfDay), dateTimeOffset.Offset);
}
- var value = new DateTime2ValueConverter.DateTime2
+ var value = new DateTime2ValueConverter.DateTime2Dto
{
Date = dateTimeOffset,
- TimeZone = valueAsJsonObject["timeZone"]?.GetValue(),
+ TimeZone = dateTime2ApiModel.TimeZone,
};
return _jsonSerializer.Serialize(value);
@@ -108,7 +111,7 @@ public DateTime2DataValueEditor(
{
var value = property.GetValue(culture, segment);
- DateTime2ValueConverter.DateTime2? interValue = DateTime2ValueConverter.GetIntermediateFromSource(value, _jsonSerializer);
+ DateTime2ValueConverter.DateTime2Dto? interValue = DateTime2ValueConverter.GetIntermediateFromSource(value, _jsonSerializer);
if (interValue is null)
{
return null;
@@ -117,10 +120,10 @@ public DateTime2DataValueEditor(
DateTime2Configuration? configuration = GetConfiguration(property.PropertyType.DataTypeKey);
var objectValue = DateTime2ValueConverter.GetObjectFromIntermediate(interValue, configuration);
- return new JsonObject
+ return new DateTime2ApiModel
{
- ["date"] = objectValue is null ? null : $"{objectValue:O}",
- ["timeZone"] = interValue.TimeZone
+ Date = objectValue is null ? null : $"{objectValue:O}",
+ TimeZone = interValue.TimeZone,
};
}
@@ -130,11 +133,12 @@ public DateTime2DataValueEditor(
///
/// Validates the date time selection for the DateTime2 property editor.
///
- private class DateTime2Validator : IValueValidator
+ private class DateTime2Validator : ITypedJsonValidator
{
private readonly ILocalizedTextService _localizedTextService;
- public DateTime2Validator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
+ public DateTime2Validator(ILocalizedTextService localizedTextService)
+ => _localizedTextService = localizedTextService;
///
public IEnumerable Validate(
@@ -171,6 +175,47 @@ public IEnumerable Validate(
["value"]);
}
}
+
+ public IEnumerable Validate(
+ DateTime2ApiModel? value,
+ DateTime2Configuration? configuration,
+ string? valueType,
+ PropertyValidationContext validationContext)
+ {
+ if (value is null)
+ {
+ yield break;
+ }
+
+ if (value.Date is null)
+ {
+ yield return new ValidationResult(
+ _localizedTextService.Localize("validation", "invalidNull"),
+ ["value"]);
+ }
+
+ if (configuration?.TimeZones?.Mode is not { } mode || mode == DateTime2Configuration.TimeZoneMode.None)
+ {
+ yield break;
+ }
+
+ if (mode == DateTime2Configuration.TimeZoneMode.Custom
+ && configuration.TimeZones.TimeZones.Any(t => t.Equals(value.TimeZone, StringComparison.InvariantCultureIgnoreCase)) != true)
+ {
+ yield return new ValidationResult(
+ _localizedTextService.Localize("validation", "notOneOfOptions", [value.TimeZone]),
+ ["value"]);
+ }
+ }
}
}
+
+ private class DateTime2ApiModel
+ {
+ [JsonPropertyName("date")]
+ public string? Date { get; set; }
+
+ [JsonPropertyName("timeZone")]
+ public string? TimeZone { get; set; }
+ }
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
index 287856476dd1..6ee8ff9b741b 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/DateTime2ValueConverter.cs
@@ -63,13 +63,13 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
/// The source value.
/// The JSON serializer.
/// The intermediate representation.
- internal static DateTime2? GetIntermediateFromSource(object? source, IJsonSerializer jsonSerializer) =>
+ internal static DateTime2Dto? GetIntermediateFromSource(object? source, IJsonSerializer jsonSerializer) =>
source switch
{
// This DateTime check is for compatibility with the "deprecated" `Umbraco.DateTime`.
// Once that is removed, this can be removed too.
- DateTime dateTime => new DateTime2 { Date = new DateTimeOffset(dateTime, TimeSpan.Zero) },
- string sourceStr => jsonSerializer.TryDeserialize(sourceStr, out DateTime2? dateTime2) ? dateTime2 : null,
+ DateTime dateTime => new DateTime2Dto { Date = new DateTimeOffset(dateTime, TimeSpan.Zero) },
+ string sourceStr => jsonSerializer.TryDeserialize(sourceStr, out DateTime2Dto? dateTime2) ? dateTime2 : null,
_ => null,
};
@@ -82,7 +82,7 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
internal static object? GetObjectFromIntermediate(object? inter, DateTime2Configuration? configuration)
{
Type propertyValueType = GetPropertyValueType(configuration);
- if (inter is not DateTime2 dateTime2)
+ if (inter is not DateTime2Dto dateTime2)
{
return propertyValueType.GetDefaultValue();
}
@@ -111,7 +111,7 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType
DateTime2Configuration? configuration,
IJsonSerializer jsonSerializer)
{
- var intermediateValue = GetIntermediateFromSource(source, jsonSerializer);
+ DateTime2Dto? intermediateValue = GetIntermediateFromSource(source, jsonSerializer);
return GetObjectFromIntermediate(intermediateValue, configuration);
}
@@ -124,7 +124,10 @@ private static Type GetPropertyValueType(DateTime2Configuration? config) =>
_ => typeof(DateTimeOffset?),
};
- public class DateTime2
+ ///
+ /// Model/DTO that represents the JSON that DateTime2 stores.
+ ///
+ internal class DateTime2Dto
{
[JsonPropertyName("date")]
public DateTimeOffset Date { get; init; }
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
index dec2fc346c67..05872b92627b 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DateTime2PropertyEditorTests.cs
@@ -111,7 +111,7 @@ public void Can_Parse_Values_From_Editor(
string? expectedTimeZone)
{
var expectedJson = expectedDateTimeOffset is null ? null : _jsonSerializer.Serialize(
- new DateTime2ValueConverter.DateTime2
+ new DateTime2ValueConverter.DateTime2Dto
{
Date = expectedDateTimeOffset.Value,
TimeZone = expectedTimeZone,
@@ -161,7 +161,7 @@ public void Can_Parse_Values_To_Editor(
{
var storedValue = offset is null
? null
- : new DateTime2ValueConverter.DateTime2
+ : new DateTime2ValueConverter.DateTime2Dto
{
Date = new DateTimeOffset(2025, 8, 20, 16, 30, 00, TimeSpan.FromHours(offset.Value)),
TimeZone = timeZone,
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
index a261871ec22e..b677c5ec4cc5 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/ValueConverters/DateTime2ValueConverterTests.cs
@@ -59,13 +59,13 @@ public void GetPropertyValueType_ReturnsExpectedType(DateTime2Configuration.Date
private static object[] _convertToIntermediateCases =
[
new object[] { null, null },
- new object[] { """{"date":"2025-08-20T16:30:00.0000000Z","timeZone":null}""", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = null } },
- new object[] { """{"date":"2025-08-20T16:30:00.0000000Z","timeZone":"Europe/Copenhagen"}""", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = "Europe/Copenhagen" } },
- new object[] { """{"date":"2025-08-20T16:30:00.0000000-05:00","timeZone":"Europe/Copenhagen"}""", new DateTime2ValueConverter.DateTime2 { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" } },
+ new object[] { """{"date":"2025-08-20T16:30:00.0000000Z","timeZone":null}""", new DateTime2ValueConverter.DateTime2Dto { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = null } },
+ new object[] { """{"date":"2025-08-20T16:30:00.0000000Z","timeZone":"Europe/Copenhagen"}""", new DateTime2ValueConverter.DateTime2Dto { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.Zero), TimeZone = "Europe/Copenhagen" } },
+ new object[] { """{"date":"2025-08-20T16:30:00.0000000-05:00","timeZone":"Europe/Copenhagen"}""", new DateTime2ValueConverter.DateTime2Dto { Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-5)), TimeZone = "Europe/Copenhagen" } },
];
[TestCaseSource(nameof(_convertToIntermediateCases))]
- public void Can_Convert_To_Intermediate_Value(string? input, DateTime2ValueConverter.DateTime2? expected)
+ public void Can_Convert_To_Intermediate_Value(string? input, DateTime2ValueConverter.DateTime2Dto? expected)
{
var result = new DateTime2ValueConverter(_jsonSerializer).ConvertSourceToIntermediate(null!, null!, input, false);
if (expected is null)
@@ -75,13 +75,13 @@ public void Can_Convert_To_Intermediate_Value(string? input, DateTime2ValueConve
}
Assert.IsNotNull(result);
- Assert.IsInstanceOf(result);
- var dateTime = (DateTime2ValueConverter.DateTime2)result;
+ Assert.IsInstanceOf(result);
+ var dateTime = (DateTime2ValueConverter.DateTime2Dto)result;
Assert.AreEqual(expected.Date, dateTime.Date);
Assert.AreEqual(expected.TimeZone, dateTime.TimeZone);
}
- private static readonly DateTime2ValueConverter.DateTime2 _convertToObjectInputDate = new()
+ private static readonly DateTime2ValueConverter.DateTime2Dto _convertToObjectInputDate = new()
{
Date = new DateTimeOffset(2025, 08, 20, 16, 30, 0, TimeSpan.FromHours(-1)),
TimeZone = "Europe/Copenhagen",
@@ -98,7 +98,7 @@ public void Can_Convert_To_Intermediate_Value(string? input, DateTime2ValueConve
[TestCaseSource(nameof(_convertToObjectCases))]
public void Can_Convert_To_Object(
- DateTime2ValueConverter.DateTime2? input,
+ DateTime2ValueConverter.DateTime2Dto? input,
DateTime2Configuration.DateTimeFormat format,
DateTime2Configuration.TimeZoneMode timeZoneMode,
object? expected)
From 4747e0aa35233f1c9d928dd566c9faa82d5970b6 Mon Sep 17 00:00:00 2001
From: Laura Neto <12862535+lauraneto@users.noreply.github.com>
Date: Fri, 5 Sep 2025 19:56:19 +0200
Subject: [PATCH 57/86] Removed unused code and updated tests
---
.../DateTime2PropertyEditor.cs | 41 +------------------
.../DateTime2PropertyEditorTests.cs | 35 ++++++++--------
.../DateTime2ValueConverterTests.cs | 9 ++--
3 files changed, 24 insertions(+), 61 deletions(-)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
index 36aef64bb8e9..075a545b211e 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/DateTime2PropertyEditor.cs
@@ -3,7 +3,6 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
-using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.IO;
@@ -140,42 +139,6 @@ private class DateTime2Validator : ITypedJsonValidator _localizedTextService = localizedTextService;
- ///