From 73e7e3fa46af6b6f6b02fae59389f4f3d7b8967e Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 2 May 2023 12:48:03 +0200 Subject: [PATCH] date input logic for types date and time, added story --- .../libs/models/index.ts | 6 + .../date-input/date-input.element.ts | 178 +++++++++++++++--- .../date-input/date-input.stories.ts | 46 +++++ .../repositories/config/config.repository.ts | 18 ++ .../src/core/mocks/browser-handlers.ts | 2 + .../src/core/mocks/domains/config.handlers.ts | 13 ++ .../src/core/mocks/e2e-handlers.ts | 2 + 7 files changed, 241 insertions(+), 24 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/repositories/config/config.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/mocks/domains/config.handlers.ts diff --git a/src/Umbraco.Web.UI.Client/libs/models/index.ts b/src/Umbraco.Web.UI.Client/libs/models/index.ts index d2fccc54c2..23a1b95c89 100644 --- a/src/Umbraco.Web.UI.Client/libs/models/index.ts +++ b/src/Umbraco.Web.UI.Client/libs/models/index.ts @@ -14,3 +14,9 @@ export interface UmbSwatchDetails { label: string; value: string; } +export interface ServertimeOffset { + /** + * offset in minutes relative to UTC + */ + offset: number; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/date-input/date-input.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/date-input/date-input.element.ts index d269cbcf71..12fe5f1717 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/date-input/date-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/date-input/date-input.element.ts @@ -1,9 +1,10 @@ -import { css, html } from 'lit'; +import { PropertyValueMap, css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { ifDefined } from 'lit/directives/if-defined.js'; import { UUIInputEvent } from '@umbraco-ui/uui'; +import { UmbConfigRepository } from '../../repositories/config/config.repository'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; @customElement('umb-date-input') @@ -14,8 +15,23 @@ export class UmbDateInputElement extends FormControlMixin(UmbLitElement) { return undefined; } + /** + * Specifies the type of input that will be rendered. + * @type {'date'| 'time'| 'datetime-local'} + * @attr + * @default date + */ + @property() + type: 'date' | 'time' | 'datetime-local' = 'date'; + @property({ type: String }) - type = 'date'; + displayValue?: string; + + @property({ type: Boolean }) + offsetTime = false; + + @state() + private _offsetValue = 0; @property({ type: String }) min?: string; @@ -26,11 +42,7 @@ export class UmbDateInputElement extends FormControlMixin(UmbLitElement) { @property({ type: Number }) step?: number; - @property({ type: Boolean }) - offsetTime = false; - - @state() - private _localValue?: string; + private _configRepository = new UmbConfigRepository(this); constructor() { super(); @@ -38,50 +50,168 @@ export class UmbDateInputElement extends FormControlMixin(UmbLitElement) { connectedCallback(): void { super.connectedCallback(); - this.offsetTime - ? (this._localValue = this.value as string) - : (this._localValue = this.#getLocal(this.value as string)); + this.offsetTime ? this.#getOffset() : (this.displayValue = this.#UTCToLocal(this.value as string)); + } + /* + connectedCallback(): void { + super.connectedCallback(); + this.offsetTime ? this.#getOffset() : (this.displayValue = this.#getLocal(this.value as string)); + } */ + + async #getOffset() { + const data = await this._configRepository.getServertimeOffset(); + if (!data) return; + this._offsetValue = data.offset; + + if (!this.value) return; + this.displayValue = this.#valueToServerOffset(this.value as string, true); } + #localToUTC(d: string) { + if (this.type === 'time') { + return new Date(`${new Date().toJSON().slice(0, 10)} ${d}`).toISOString().slice(11, 16); + } else { + const date = new Date(d); + const isoDate = date.toISOString(); + return `${isoDate.substring(0, 10)}T${isoDate.substring(11, 19)}Z`; + } + } + + #UTCToLocal(d: string) { + if (this.type === 'time') { + const local = new Date(`${new Date().toJSON().slice(0, 10)} ${d}Z`) + .toLocaleTimeString(undefined, { + hourCycle: 'h23', + }) + .slice(0, 5); + return local; + } else { + const timezoneReset = `${d.replace('Z', '')}Z`; + const date = new Date(timezoneReset); + + const dateString = `${date.getFullYear()}-${('0' + (date.getMonth() + 1)).slice(-2)}-${( + '0' + date.getDate() + ).slice(-2)}T${('0' + date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}:${( + '0' + date.getSeconds() + ).slice(-2)}`; + + return this.type === 'datetime-local' ? dateString : `${dateString.substring(0, 10)}`; + } + } + + #dateToString(date: Date) { + return `${date.getFullYear()}-${('0' + (date.getMonth() + 1)).slice(-2)}-${('0' + date.getDate()).slice(-2)}T${( + '0' + date.getHours() + ).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}:${('0' + date.getSeconds()).slice(-2)}`; + } + + #valueToServerOffset(d: string, utc = false) { + if (this.type === 'time') { + return ''; + } else { + const newDate = new Date(d.replace('Z', '')); + const dateOffset = new Date( + newDate.setTime(newDate.getTime() + (utc ? this._offsetValue * -1 : this._offsetValue) * 60 * 1000) + ); + return this.type === 'datetime-local' + ? this.#dateToString(dateOffset) + : this.#dateToString(dateOffset).slice(0, 10); + } + } + + #onChange(e: UUIInputEvent) { + e.stopPropagation(); + const picked = e.target.value as string; + this.value = picked ? this.#localToUTC(picked) : ''; + this.displayValue = picked ? picked : ''; + this.dispatchEvent(new CustomEvent('change')); + } + + /* + + #valueToServerOffset(date: string, utc = false) { + const newDate = new Date(date); + const dateOffset = new Date( + newDate.setTime(newDate.getTime() + (utc ? this._offsetValue * -1 : this._offsetValue) * 60 * 1000) + ); + } + + async #getOffset() { + const data = await this._configRepository.getServertimeOffset(); + if (!data) return; + this._offsetValue = data.offset; + + if (!this.value) return; + this.displayValue = this.#convertValueToServerOffset((this.value as string).replace('Z', ''), true); + } + + /** Converts a Date object to string in the correct format for input field */ + /* + #convertDateToString(date: Date) { + if (!date) return; + const string = `${date.getFullYear()}-${('0' + (date.getMonth() + 1)).slice(-2)}-${('0' + date.getDate()).slice( + -2 + )}T${('0' + date.getHours()).slice(-2)}:${('0' + date.getMinutes()).slice(-2)}:${('0' + date.getSeconds()).slice( + -2 + )}Z`; + return string; + } + */ + /* + #convertValueToServerOffset(date: string, utc?: boolean) { + if (!date) return; + const newDate = new Date(date); + // + offsetValue (minutes) * seconds * miliseconds + const dateOffset = new Date( + newDate.setTime(newDate.getTime() + (utc ? this._offsetValue * -1 : this._offsetValue) * 60 * 1000) + ); + console.log(dateOffset); + return this.#convertDateToString(dateOffset); + } + */ + /* #getUTC(timeLocal: string) { + if (!timeLocal) return; const date = new Date(timeLocal); const isoDate = date.toISOString(); return `${isoDate.substring(0, 10)}T${isoDate.substring(11, 19)}Z`; } #getLocal(timeUTC: string) { + if (!timeUTC) return; const local = new Date(timeUTC); - const localString = `${local.getFullYear()}-${('0' + (local.getMonth() + 1)).slice(-2)}-${( - '0' + local.getDate() - ).slice(-2)}T${('0' + local.getHours()).slice(-2)}:${('0' + local.getMinutes()).slice(-2)}:${( - '0' + local.getSeconds() - ).slice(-2)}Z`; - return localString; + return this.#convertDateToString(local); } - +*/ + /* #onDatetimeChange(e: UUIInputEvent) { e.stopPropagation(); const pickedTime = e.target.value as string; - this.offsetTime ? (this.value = pickedTime) : (this.value = this.#getUTC(pickedTime)); + this.displayValue = pickedTime; + + const val = this.offsetTime ? this.#convertValueToServerOffset(pickedTime) : this.#getUTC(pickedTime); + this.value = val ?? ''; - this._localValue = pickedTime; this.dispatchEvent(new CustomEvent('change')); } + */ + //@change="${this.#onDatetimeChange}" render() { return html` -

+ .value="${this.displayValue?.replace('Z', '')}"> + +
UTC: ${this.value}
- Local ${this._localValue}
- offset: ${this.offsetTime}`; + Display: ${this.displayValue}
+ Offset: ${this._offsetValue}
`; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/date-input/date-input.stories.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/date-input/date-input.stories.ts index 85458a242a..381f55073c 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/date-input/date-input.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/date-input/date-input.stories.ts @@ -1,4 +1,5 @@ import { Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit'; import './date-input.element'; import type { UmbDateInputElement } from './date-input.element'; @@ -17,3 +18,48 @@ export const Overview: Story = { offsetTime: true, }, }; + +export const Date: Story = { + args: { + type: 'date', + value: '2023-04-01', + offsetTime: true, + }, +}; + +export const Time: Story = { + args: { + type: 'time', + value: '10:00', + }, +}; + +export const DatetimelocalOffset: Story = { + args: { + type: 'datetime-local', + value: '2023-04-01T10:00:00', + offsetTime: false, + displayValue: '', + }, + render: (args) => + html``, +}; + +export const Datetimelocal: Story = { + args: { + type: 'datetime-local', + value: '2023-04-01T10:00:00', + offsetTime: false, + displayValue: '', + }, + render: (args) => + html``, +}; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/repositories/config/config.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/repositories/config/config.repository.ts new file mode 100644 index 0000000000..1f7aef941a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/repositories/config/config.repository.ts @@ -0,0 +1,18 @@ +import { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller'; +import type { ServertimeOffset } from '@umbraco-cms/backoffice/models'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; + +export class UmbConfigRepository { + #host; + + constructor(host: UmbControllerHostElement) { + this.#host = host; + } + + async getServertimeOffset() { + const resource = fetch(umbracoPath('/config/servertimeoffset')).then((res) => res.json()); + const { data } = await tryExecuteAndNotify(this.#host, resource); + return data; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts index e899c2aaf6..a985c05391 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts @@ -29,6 +29,7 @@ import { handlers as logViewerHandlers } from './domains/log-viewer.handlers'; import { handlers as packageHandlers } from './domains/package.handlers'; import { handlers as rteEmbedHandlers } from './domains/rte-embed.handlers'; import { handlers as stylesheetHandlers } from './domains/stylesheet.handlers'; +import { handlers as configHandlers } from './domains/config.handlers'; const handlers = [ serverHandlers.serverVersionHandler, @@ -61,6 +62,7 @@ const handlers = [ ...packageHandlers, ...rteEmbedHandlers, ...stylesheetHandlers, + ...configHandlers, ]; switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/config.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/config.handlers.ts new file mode 100644 index 0000000000..800d803b10 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/config.handlers.ts @@ -0,0 +1,13 @@ +import { rest } from 'msw'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +import type { ServertimeOffset } from '@umbraco-cms/backoffice/models'; + +export const handlers = [ + rest.get(umbracoPath('/config/servertimeoffset'), (_req, res, ctx) => { + return res( + // Respond with a 200 status code + ctx.status(200), + ctx.json({ offset: -120 }) + ); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts index e0a5d8efe6..9e02222276 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts @@ -14,6 +14,7 @@ import { handlers as healthCheckHandlers } from './domains/health-check.handlers import { handlers as languageHandlers } from './domains/language.handlers'; import { handlers as redirectManagementHandlers } from './domains/redirect-management.handlers'; import { handlers as packageHandlers } from './domains/package.handlers'; +import { handlers as configHandlers } from './domains/config.handlers'; export const handlers = [ serverHandlers.serverRunningHandler, @@ -33,4 +34,5 @@ export const handlers = [ ...languageHandlers, ...redirectManagementHandlers, ...packageHandlers, + ...configHandlers, ];