From fc2cf1bd6bde51354be1614c4d6f9a00ed3df637 Mon Sep 17 00:00:00 2001 From: Matthew Evans Date: Sat, 26 Oct 2024 18:23:55 +0200 Subject: [PATCH] Update app to use localised timezones everywhere Remove now-unnecessary timezone workaround in app Update app test with UTC offset Fix app bug introduced during refactor Add specific handling of datetime fields in computed getters/setters Use localized timestamp in UI, thanks to Sweden Fix UI date display when updated in starting_materials EditPage --- webapp/cypress/e2e/equipment.cy.js | 2 +- .../StartingMaterialInformation.vue | 2 +- webapp/src/field_utils.js | 73 +++++++++++++++++-- webapp/src/resources.js | 3 + webapp/src/views/CollectionPage.vue | 4 +- webapp/src/views/EditPage.vue | 4 +- 6 files changed, 73 insertions(+), 15 deletions(-) diff --git a/webapp/cypress/e2e/equipment.cy.js b/webapp/cypress/e2e/equipment.cy.js index b30e65359..7f042b647 100644 --- a/webapp/cypress/e2e/equipment.cy.js +++ b/webapp/cypress/e2e/equipment.cy.js @@ -67,7 +67,7 @@ describe("Equipment table page", () => { expect(body).to.have.property("item_id", "test_e3"); expect(body.item_data).to.have.property("item_id", "test_e3"); expect(body.item_data).to.have.property("name", "my inst"); - expect(body.item_data).to.have.property("date", "1990-01-07T00:00:00"); + expect(body.item_data).to.have.property("date", "1990-01-07T00:00:00+00:00"); }); }); diff --git a/webapp/src/components/StartingMaterialInformation.vue b/webapp/src/components/StartingMaterialInformation.vue index 76f71b6e8..118f7db1e 100644 --- a/webapp/src/components/StartingMaterialInformation.vue +++ b/webapp/src/components/StartingMaterialInformation.vue @@ -38,7 +38,7 @@ diff --git a/webapp/src/field_utils.js b/webapp/src/field_utils.js index c34f3e1bc..2b64bf564 100644 --- a/webapp/src/field_utils.js +++ b/webapp/src/field_utils.js @@ -1,5 +1,53 @@ import store from "@/store/index.js"; -//import { debounce } from 'lodash'; +import { DATETIME_FIELDS } from "@/resources.js"; + +/** + * Converts a datetime-local input value to an ISO format string with timezone offset. + * + * @param {string} value - The datetime value from a datetime-local input + * (format: "YYYY-MM-DDThh:mm") + * + * @returns {string} ISO format datetime with timezone offset + * (format: "YYYY-MM-DDThh:mm:ss±hh:mm") + * + */ +export function dateTimeFormatter(value) { + if (!value) return ""; + try { + const date = new Date(value); + const tzOffset = date.getTimezoneOffset(); + const tzHours = Math.abs(Math.floor(tzOffset / 60)); + const tzMinutes = Math.abs(tzOffset % 60); + const tzSign = tzOffset > 0 ? "-" : "+"; + const tzString = `${tzSign}${tzHours.toString().padStart(2, "0")}:${tzMinutes + .toString() + .padStart(2, "0")}`; + + return `${value}:00${tzString}`; + } catch (err) { + return ""; + } +} + +/** + * Converts an ISO format datetime string with timezone offset to a datetime-local input value. + * + * @param {string} value - The ISO format datetime with timezone offset (format: "YYYY-MM-DDThh:mm:ss±hh:mm") + * @returns {string} datetime-local input value (format: "YYYY-MM-DDThh:mm") + * + */ +export function dateTimeParser(value) { + if (!value) return ""; + try { + const date = new Date(value); + // The Swedes are sensible and use basically the isoformat for their locale string; we take advantage of this + // to get an ISO datetime that does not use UTC; the 'iso' locale itself has slashes and commas for some reason + return date.toLocaleString("sv").replace(" ", "T").slice(0, 16); + } catch (err) { + console.error("Invalid date passed to dateTimeParser", value); + return ""; + } +} // Amazingly (and perhaps dangerously) the this context used here is the this from // the component which this function is called for. @@ -27,17 +75,23 @@ export function createComputedSetterForItemField(item_field) { return { get() { if (this.item_id in store.state.all_item_data) { - return store.state.all_item_data[this.item_id][item_field]; + let value = store.state.all_item_data[this.item_id][item_field]; + if (DATETIME_FIELDS.has(item_field)) { + value = dateTimeParser(value); + } + + return value; } }, set(value) { - //set: debounce(function(value) { console.log(`comp setter called for '${item_field}' with value: '${value}'`); + if (DATETIME_FIELDS.has(item_field)) { + value = dateTimeFormatter(value); + } store.commit("updateItemData", { item_id: this.item_id, item_data: { [item_field]: value === "" ? null : value }, }); - //}, 500), }, }; } @@ -46,17 +100,22 @@ export function createComputedSetterForCollectionField(collection_field) { return { get() { if (this.collection_id in store.state.all_collection_data) { - return store.state.all_collection_data[this.collection_id][collection_field]; + let value = store.state.all_collection_data[this.collection_id][collection_field]; + if (DATETIME_FIELDS.has(collection_field)) { + value = dateTimeParser(value); + } + return value; } }, set(value) { - //set: debounce( function(value) { + if (DATETIME_FIELDS.has(collection_field)) { + value = dateTimeFormatter(value); + } console.log(`collection comp setter called for '${collection_field}' with value: '${value}'`); store.commit("updateCollectionData", { collection_id: this.collection_id, block_data: { [collection_field]: value }, }); - //}, 500), }, }; } diff --git a/webapp/src/resources.js b/webapp/src/resources.js index babd44ed9..f61bd005f 100644 --- a/webapp/src/resources.js +++ b/webapp/src/resources.js @@ -39,6 +39,9 @@ export const GRAVATAR_STYLE = "identicon"; const editable_inventory = process.env.VUE_APP_EDITABLE_INVENTORY || "false"; export const EDITABLE_INVENTORY = editable_inventory.toLowerCase() == "true"; +// Eventually this should be pulled from the schema +export const DATETIME_FIELDS = new Set(["date"]); + export const UPPY_MAX_TOTAL_FILE_SIZE = Number(process.env.VUE_APP_UPPY_MAX_TOTAL_FILE_SIZE) != null ? process.env.VUE_APP_UPPY_MAX_TOTAL_FILE_SIZE diff --git a/webapp/src/views/CollectionPage.vue b/webapp/src/views/CollectionPage.vue index e0309695d..68db29f58 100644 --- a/webapp/src/views/CollectionPage.vue +++ b/webapp/src/views/CollectionPage.vue @@ -144,9 +144,7 @@ export default { if (item_date == null) { this.lastModified = "Unknown"; } else { - // API dates are in UTC but missing Z suffix - const save_date = new Date(item_date + "Z"); - this.lastModified = formatDistanceToNow(save_date, { addSuffix: true }); + this.lastModified = formatDistanceToNow(new Date(item_date), { addSuffix: true }); } }, }, diff --git a/webapp/src/views/EditPage.vue b/webapp/src/views/EditPage.vue index 399f5a6aa..7c6af57d7 100644 --- a/webapp/src/views/EditPage.vue +++ b/webapp/src/views/EditPage.vue @@ -310,9 +310,7 @@ export default { if (item_date == null) { this.lastModified = "Unknown"; } else { - // API dates are in UTC but missing Z suffix - const save_date = new Date(item_date + "Z"); - this.lastModified = formatDistanceToNow(save_date, { addSuffix: true }); + this.lastModified = formatDistanceToNow(new Date(item_date), { addSuffix: true }); } }, },