fix(web): preserve timezone when changing timestamp (Closes #25354) (#27095)

This commit is contained in:
Michael Maycock
2026-03-26 17:30:47 +00:00
committed by GitHub
parent 5c159d70a7
commit eeb55c279b
2 changed files with 80 additions and 5 deletions
@@ -0,0 +1,67 @@
import { getAnimateMock } from '$lib/__mocks__/animate.mock';
import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock';
import { getVisualViewportMock } from '$lib/__mocks__/visual-viewport.mock';
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
import { DateTime } from 'luxon';
import { afterAll, beforeEach, describe, expect, test, vi } from 'vitest';
import AssetChangeDateModal from './AssetChangeDateModal.svelte';
describe('AssetChangeDateModal component', () => {
const initialDate = DateTime.fromISO('2026-03-19T23:31:30.112');
const initialTimeZone = 'Europe/Lisbon';
const onClose = vi.fn();
const getDateInput = async () => (await screen.findByDisplayValue('2026-03-19T23:31:30.112')) as HTMLInputElement;
const getTimeZoneInput = () => screen.getByRole('combobox', { name: /timezone/i }) as HTMLInputElement;
beforeEach(() => {
vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
vi.stubGlobal('visualViewport', getVisualViewportMock());
vi.resetAllMocks();
Element.prototype.animate = getAnimateMock();
});
afterAll(async () => {
await waitFor(() => {
expect(document.body.style.pointerEvents).not.toBe('none');
});
});
test('preserves the selected timezone when changing the datetime', async () => {
render(AssetChangeDateModal, {
props: {
initialDate,
initialTimeZone,
timezoneInput: true,
asset: { id: 'asset-id' } as never,
onClose,
},
});
const timezoneInput = getTimeZoneInput();
const datetimeInput = await getDateInput();
const initialTimezoneValue = timezoneInput.value;
await fireEvent.focus(timezoneInput);
await fireEvent.input(timezoneInput, { target: { value: 'Pacific/Pitcairn' } });
const option = await screen.findByText(/Pacific\/Pitcairn/i);
await fireEvent.click(option);
expect(timezoneInput.value).toBe('Pacific/Pitcairn (-08:00)');
expect(timezoneInput.value).not.toBe(initialTimezoneValue);
const beforeDatetime = datetimeInput.value;
await fireEvent.input(datetimeInput, {
target: { value: '2026-03-19T23:31:31.113' },
});
await fireEvent.change(datetimeInput, {
target: { value: '2026-03-19T23:31:31.113' },
});
expect(datetimeInput.value).not.toBe(beforeDatetime);
expect(timezoneInput.value).toBe('Pacific/Pitcairn (-08:00)');
});
});
+13 -5
View File
@@ -23,10 +23,7 @@
let selectedDate = $state(initialDate.toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"));
const timezones = $derived(getTimezones(selectedDate));
// svelte-ignore state_referenced_locally
let lastSelectedTimezone = $state(getPreferredTimeZone(initialDate, initialTimeZone, timezones));
// the offsets (and validity) for time zones may change if the date is changed, which is why we recompute the list
let selectedOption = $derived(getPreferredTimeZone(initialDate, initialTimeZone, timezones, lastSelectedTimezone));
let selectedOption = $state(getPreferredTimeZone(initialDate, initialTimeZone, getTimezones(selectedDate)));
const onSubmit = async () => {
if (!date.isValid || !selectedOption) {
@@ -45,6 +42,12 @@
}
};
const updateSelectedDate = (value: string) => {
selectedDate = value;
selectedOption = getPreferredTimeZone(initialDate, initialTimeZone, getTimezones(value), selectedOption);
};
// when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it)
const date = $derived(DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }));
</script>
@@ -59,7 +62,12 @@
size="small"
>
<Label for="datetime" class="block mb-1">{$t('date_and_time')}</Label>
<DateInput class="immich-form-input w-full mb-2" id="datetime" type="datetime-local" bind:value={selectedDate} />
<DateInput
class="immich-form-input w-full mb-2"
id="datetime"
type="datetime-local"
bind:value={() => selectedDate, updateSelectedDate}
/>
{#if timezoneInput}
<div class="w-full">
<Combobox bind:selectedOption label={$t('timezone')} options={timezones} placeholder={$t('search_timezone')} />