diff --git a/web/package-lock.json b/web/package-lock.json index 9b8e3eee8..b78204f24 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7,6 +7,7 @@ "name": "agama", "license": "LGPL-2.1", "dependencies": { + "@date-fns/tz": "^1.1.2", "@icons-pack/react-simple-icons": "^10.0.0", "@material-symbols/svg-400": "^0.23.0", "@patternfly/patternfly": "^5.1.0", @@ -2754,6 +2755,12 @@ "postcss-selector-parser": "^6.1.0" } }, + "node_modules/@date-fns/tz": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.1.2.tgz", + "integrity": "sha512-Xmg2cPmOPQieCLAdf62KtFPU9y7wbQDq1OAzrs/bEQFvhtCPXDiks1CHDE/sTXReRfh/MICVkw/vY6OANHUGiA==", + "license": "MIT" + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", diff --git a/web/package.json b/web/package.json index 7a8503500..a32770c1d 100644 --- a/web/package.json +++ b/web/package.json @@ -107,6 +107,7 @@ "webpack-dev-server": "^5.0.4" }, "dependencies": { + "@date-fns/tz": "^1.1.2", "@icons-pack/react-simple-icons": "^10.0.0", "@material-symbols/svg-400": "^0.23.0", "@patternfly/patternfly": "^5.1.0", diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 65f79273e..8c310c0cd 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Mon Sep 30 08:52:36 UTC 2024 - Imobach Gonzalez Sosa + +- Fix timezones UTC offset calculation (gh#agama-project/agama#1335). + ------------------------------------------------------------------- Fri Sep 27 14:54:46 UTC 2024 - Imobach Gonzalez Sosa diff --git a/web/src/api/l10n.ts b/web/src/api/l10n.ts index b403dc62f..224cae170 100644 --- a/web/src/api/l10n.ts +++ b/web/src/api/l10n.ts @@ -20,9 +20,9 @@ * find current contact information at www.suse.com. */ +import { tzOffset } from "@date-fns/tz/tzOffset"; import { get, patch } from "~/api/http"; import { Keymap, Locale, LocaleConfig, Timezone } from "~/types/l10n"; -import { timezoneUTCOffset } from "~/utils"; /** * Returns the l10n configuration @@ -45,7 +45,7 @@ const fetchLocales = async (): Promise => { const fetchTimezones = async (): Promise => { const json = await get("/api/l10n/timezones"); return json.map(({ code, parts, country }): Timezone => { - const offset = timezoneUTCOffset(code); + const offset = tzOffset(code, new Date()); return { id: code, parts, country, utcOffset: offset }; }); }; diff --git a/web/src/components/l10n/TimezoneSelection.test.tsx b/web/src/components/l10n/TimezoneSelection.test.tsx index 64539c665..201595f4e 100644 --- a/web/src/components/l10n/TimezoneSelection.test.tsx +++ b/web/src/components/l10n/TimezoneSelection.test.tsx @@ -27,8 +27,20 @@ import { screen } from "@testing-library/react"; import { mockNavigateFn, plainRender } from "~/test-utils"; const timezones = [ - { id: "Europe/Berlin", parts: ["Europe", "Berlin"], country: "Germany", utcOffset: 1 }, - { id: "Europe/Madrid", parts: ["Europe", "Madrid"], country: "Spain", utfOffset: 1 }, + { id: "Europe/Berlin", parts: ["Europe", "Berlin"], country: "Germany", utcOffset: 120 }, + { id: "Europe/Madrid", parts: ["Europe", "Madrid"], country: "Spain", utcOffset: 120 }, + { + id: "Australia/Adelaide", + parts: ["Australia", "Adelaide"], + country: "Australia", + utcOffset: 570, + }, + { + id: "America/Antigua", + parts: ["Americas", "Caracas"], + country: "Antigua & Barbuda", + utcOffset: -240, + }, ]; const mockConfigMutation = { @@ -46,13 +58,33 @@ jest.mock("react-router-dom", () => ({ useNavigate: () => mockNavigateFn, })); -it("allows changing the keyboard", async () => { +beforeEach(() => { + const mockedDate = new Date(2024, 6, 1, 12, 0); + + jest.useFakeTimers(); + jest.setSystemTime(mockedDate); +}); + +afterEach(() => { + jest.useRealTimers(); +}); + +it("allows changing the timezone", async () => { plainRender(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); const option = await screen.findByText("Europe-Madrid"); - await userEvent.click(option); + await user.click(option); const button = await screen.findByRole("button", { name: "Select" }); - await userEvent.click(button); + await user.click(button); expect(mockConfigMutation.mutate).toHaveBeenCalledWith({ timezone: "Europe/Madrid" }); expect(mockNavigateFn).toHaveBeenCalledWith(-1); }); + +it("displays the UTC offset", () => { + plainRender(); + + expect(screen.getByText("Australia/Adelaide UTC+9:30")).toBeInTheDocument(); + expect(screen.getByText("Europe/Madrid UTC+2")).toBeInTheDocument(); + expect(screen.getByText("America/Antigua UTC-4")).toBeInTheDocument(); +}); diff --git a/web/src/components/l10n/TimezoneSelection.tsx b/web/src/components/l10n/TimezoneSelection.tsx index c5dcd733d..0ea385a2b 100644 --- a/web/src/components/l10n/TimezoneSelection.tsx +++ b/web/src/components/l10n/TimezoneSelection.tsx @@ -39,9 +39,16 @@ const timezoneWithDetails = (timezone: Timezone): TimezoneWithDetails => { if (offset === undefined) return { ...timezone, details: timezone.id }; + const hours = Math.floor(offset / 60); + const minutes = offset % 60; + const hoursString = hours >= 0 ? `+${hours}` : `${hours}`; + let utc = "UTC"; - if (offset > 0) utc += `+${offset}`; - if (offset < 0) utc += `${offset}`; + if (minutes === 0) { + utc += hoursString; + } else { + utc += `${hoursString}:${minutes}`; + } return { ...timezone, details: `${timezone.id} ${utc}` }; }; diff --git a/web/src/utils.js b/web/src/utils.js index 8c8293582..177b2e8b9 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -420,31 +420,6 @@ const timezoneTime = (timezone, { date = new Date() }) => { } }; -/** - * UTC offset for the given timezone. - * - * @param {string} timezone - E.g., "Atlantic/Canary". - * @returns {number|undefined} - undefined for an unknown timezone. - */ -const timezoneUTCOffset = (timezone) => { - try { - const date = new Date(); - const dateLocaleString = date.toLocaleString("en-US", { - timeZone: timezone, - timeZoneName: "short", - }); - const [timezoneName] = dateLocaleString.split(" ").slice(-1); - const dateString = date.toString(); - const offset = Date.parse(`${dateString} UTC`) - Date.parse(`${dateString} ${timezoneName}`); - - return offset / 3600000; - } catch (e) { - if (e instanceof RangeError) return undefined; - - throw e; - } -}; - export { noop, identity, @@ -466,5 +441,4 @@ export { remoteConnection, slugify, timezoneTime, - timezoneUTCOffset, };