From 2256a263274db92a5d3eff018f9acc30e95b73be Mon Sep 17 00:00:00 2001 From: nathan Date: Mon, 18 Oct 2021 16:03:05 -0600 Subject: [PATCH] Make date widget work a little better WRT timezones --- api/js/etemplate/Et2Date/Et2Date.ts | 29 +++++++++-- api/js/etemplate/Et2Date/test/Et2Date.test.ts | 49 ++++++++++++++++--- api/js/etemplate/Et2Widget/Et2Widget.ts | 21 +++++--- 3 files changed, 82 insertions(+), 17 deletions(-) diff --git a/api/js/etemplate/Et2Date/Et2Date.ts b/api/js/etemplate/Et2Date/Et2Date.ts index 754348ee3e..06d7a4ca6e 100644 --- a/api/js/etemplate/Et2Date/Et2Date.ts +++ b/api/js/etemplate/Et2Date/Et2Date.ts @@ -105,7 +105,7 @@ export function formatDate(date: Date, options): string } let _value = ''; // Add timezone offset back in, or formatDate will lose those hours - let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000); + let formatDate = new Date(date.valueOf() - date.getTimezoneOffset() * 60 * 1000); let dateformat = options.dateFormat || egw.preference("dateformat") || 'Y-m-d'; @@ -179,14 +179,18 @@ export class Et2Date extends Et2InputWidget(LionInputDatepicker) return new Date(modelValue.getTime() - offset).toJSON().replace(/\.\d{3}Z$/, 'Z'); } + set_value(value) + { + this.modelValue = this.parser(value); + } + getValue() { // The supplied value was not understandable, return null - if(this.modelValue instanceof Unparseable) + if(this.modelValue instanceof Unparseable || !this.modelValue) { return null; } - return this.modelValue.toJSON(); } @@ -216,12 +220,29 @@ export class Et2Date extends Et2InputWidget(LionInputDatepicker) return super.withBottomSheetConfig(); } + /** + * The LionCalendar shouldn't know anything about the modelValue; + * it can't handle Unparseable dates, but does handle 'undefined' + * @param {?} modelValue + * @returns {Date|undefined} a 'guarded' modelValue + */ + static __getSyncDownValue(modelValue) + { + if(!(modelValue instanceof Date)) + { + return undefined; + } + const offset = modelValue.getTimezoneOffset() * 60000; + return new Date(modelValue.getTime() + offset); + } + /** * Overriding parent to add class to button, and use an image instead of unicode emoji */ // eslint-disable-next-line class-methods-use-this _invokerTemplate() { + let img = this.egw() ? this.egw().image("calendar") || '' : ''; return html` `; } diff --git a/api/js/etemplate/Et2Date/test/Et2Date.test.ts b/api/js/etemplate/Et2Date/test/Et2Date.test.ts index 0a17c118b3..153ac68a01 100644 --- a/api/js/etemplate/Et2Date/test/Et2Date.test.ts +++ b/api/js/etemplate/Et2Date/test/Et2Date.test.ts @@ -26,6 +26,12 @@ describe("Date widget", () => // Image always give check mark. Use data URL to avoid having to serve an actual image image: i => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=" }); + + // Stub global egw for preference + // @ts-ignore + window.egw = { + preference: () => 'Y-m-d' + }; }); // Make sure it works @@ -41,21 +47,52 @@ describe("Date widget", () => assert.equal(element.querySelector("[slot='label']").textContent, "Label set"); }) - const tz_list = ['US/Pacific','UTC','Australia/Adelaide']; + const tz_list = [ + {name: "America/Edmonton", offset: 600}, + {name: "UTC", offset: 0}, + {name: "Australia/Adelaide", offset: 630} + ]; for(let tz of tz_list) { - // TODO: Figure out how to mock timezone... - describe("Timezone: " + tz, () => + describe("Timezone: " + tz.name, () => { - let test_time_string = '2008-09-22 12:00:00Z'; + // TODO: Figure out how to mock timezone... + // Stub timezone offset to return a different value + let tz_offset_stub = sinon.stub(Date.prototype, "getTimezoneOffset").returns( + tz.offset + ); + let test_time_string = '2008-09-22T12:00:00.000Z'; let test_time = new Date(test_time_string); it('Can accept a value', () => { element.set_value(test_time_string); - // Widget gives time as a string so we can send to server - assert(element.getValue(), test_time_string); + // Use a Promise to wait for asychronous changes to the DOM + return Promise.resolve().then(() => + { + // Widget gives time as a string so we can send to server + assert.equal(element.getValue(), test_time_string); + }); }); + + /* Doesn't work yet + it("Can be modified", () => + { + element.getInputNode().value = "2008-09-22"; + let event = new Event("change"); + element.getInputNode().dispatchEvent(event); + + // Use a Promise to wait for asychronous changes to the DOM + return Promise.resolve().then(() => + { + assert.equal(element.getValue(), "2008-09-22T00:00:00.000Z"); + }); + }); + + */ + + // Put timezone offset back + tz_offset_stub.restore(); }); } }); \ No newline at end of file diff --git a/api/js/etemplate/Et2Widget/Et2Widget.ts b/api/js/etemplate/Et2Widget/Et2Widget.ts index 057164e2e7..6e3e3f4d01 100644 --- a/api/js/etemplate/Et2Widget/Et2Widget.ts +++ b/api/js/etemplate/Et2Widget/Et2Widget.ts @@ -212,13 +212,20 @@ const Et2WidgetMixin = (superClass) => this._label = value; if(value) { - let label = document.createElement("span"); - label.classList.add("et2_label"); - label.textContent = this._label; - // We should have a slot in the template for the label - //label.slot="label"; - this.appendChild(label); - this.requestUpdate('label', oldValue); + if(this._labelNode) + { + this._labelNode.textContent = this._label; + } + else + { + let label = document.createElement("span"); + label.classList.add("et2_label"); + label.textContent = this._label; + // We should have a slot in the template for the label + //label.slot="label"; + this.appendChild(label); + this.requestUpdate('label', oldValue); + } } }