Skip to content

Commit 0fa08ed

Browse files
committed
fix: navigate to time action
1 parent 8473dab commit 0fa08ed

File tree

10 files changed

+242
-132
lines changed

10 files changed

+242
-132
lines changed

i18n/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,8 @@
13641364
"my_albums": "My albums",
13651365
"name": "Name",
13661366
"name_or_nickname": "Name or nickname",
1367+
"navigate": "Navigate",
1368+
"navigate_to_time": "Navigate to Time",
13671369
"network_requirement_photos_upload": "Use cellular data to backup photos",
13681370
"network_requirement_videos_upload": "Use cellular data to backup videos",
13691371
"network_requirements": "Network Requirements",

web/src/lib/components/asset-viewer/detail-panel.svelte

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
66
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
77
import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
8-
import ChangeDate, {
9-
type AbsoluteResult,
10-
type RelativeResult,
11-
} from '$lib/components/shared-components/change-date.svelte';
8+
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
129
import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants';
1310
import { authManager } from '$lib/managers/auth-manager.svelte';
1411
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
@@ -24,7 +21,7 @@
2421
import { fromISODateTime, fromISODateTimeUTC } from '$lib/utils/timeline-util';
2522
import { getParentPath } from '$lib/utils/tree-utils';
2623
import { AssetMediaSize, getAssetInfo, updateAsset, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
27-
import { Icon, IconButton, LoadingSpinner } from '@immich/ui';
24+
import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui';
2825
import {
2926
mdiCalendar,
3027
mdiCameraIris,
@@ -112,18 +109,24 @@
112109
113110
const toggleAssetPath = () => (showAssetPath = !showAssetPath);
114111
115-
let isShowChangeDate = $state(false);
116-
117-
async function handleConfirmChangeDate(result: AbsoluteResult | RelativeResult) {
118-
isShowChangeDate = false;
112+
const showChangeDate = async () => {
113+
const result = await modalManager.show(ChangeDate, {
114+
initialDate: dateTime,
115+
initialTimeZone: timeZone ?? '',
116+
withDuration: false,
117+
timezoneInput: false,
118+
});
119+
if (!result) {
120+
return;
121+
}
119122
try {
120123
if (result.mode === 'absolute') {
121124
await updateAsset({ id: asset.id, updateAssetDto: { dateTimeOriginal: result.date } });
122125
}
123126
} catch (error) {
124127
handleError(error, $t('errors.unable_to_change_date'));
125128
}
126-
}
129+
};
127130
</script>
128131

129132
<section class="relative p-2">
@@ -280,7 +283,7 @@
280283
<button
281284
type="button"
282285
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
283-
onclick={() => (isOwner ? (isShowChangeDate = true) : null)}
286+
onclick={() => (isOwner ? showChangeDate() : null)}
284287
title={isOwner ? $t('edit_date') : ''}
285288
class:hover:text-primary={isOwner}
286289
>
@@ -336,16 +339,6 @@
336339
</div>
337340
{/if}
338341

339-
{#if isShowChangeDate}
340-
<ChangeDate
341-
initialDate={dateTime}
342-
initialTimeZone={timeZone ?? ''}
343-
withDuration={false}
344-
onConfirm={handleConfirmChangeDate}
345-
onCancel={() => (isShowChangeDate = false)}
346-
/>
347-
{/if}
348-
349342
<div class="flex gap-4 py-4">
350343
<div><Icon icon={mdiImageOutline} size="24" /></div>
351344

web/src/lib/components/shared-components/change-date.spec.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@ import ChangeDate from './change-date.svelte';
88
describe('ChangeDate component', () => {
99
const initialDate = DateTime.fromISO('2024-01-01');
1010
const initialTimeZone = 'Europe/Berlin';
11+
const targetDate = DateTime.fromISO('2024-01-01').setZone('UTC+1', {
12+
keepLocalTime: true,
13+
});
1114
const currentInterval = {
1215
start: DateTime.fromISO('2000-02-01T14:00:00+01:00'),
1316
end: DateTime.fromISO('2001-02-01T14:00:00+01:00'),
1417
};
15-
const onCancel = vi.fn();
16-
const onConfirm = vi.fn();
18+
const onClose = vi.fn();
1719

1820
const getRelativeInputToggle = () => screen.getByTestId('edit-by-offset-switch');
1921
const getDateInput = () => screen.getByLabelText('date_and_time') as HTMLInputElement;
2022
const getTimeZoneInput = () => screen.getByLabelText('timezone') as HTMLInputElement;
21-
const getCancelButton = () => screen.getByText('Cancel');
22-
const getConfirmButton = () => screen.getByText('Confirm');
23+
const getCancelButton = () => screen.getByText('cancel');
24+
const getConfirmButton = () => screen.getByText('confirm');
2325

2426
beforeEach(() => {
2527
vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock());
@@ -38,54 +40,64 @@ describe('ChangeDate component', () => {
3840
});
3941

4042
test('should render correct values', () => {
41-
render(ChangeDate, { initialDate, initialTimeZone, onCancel, onConfirm });
43+
render(ChangeDate, { initialDate, initialTimeZone, onClose });
4244
expect(getDateInput().value).toBe('2024-01-01T00:00');
4345
expect(getTimeZoneInput().value).toBe('Europe/Berlin (+01:00)');
4446
});
4547

4648
test('calls onConfirm with correct date on confirm', async () => {
4749
render(ChangeDate, {
48-
props: { initialDate, initialTimeZone, onCancel, onConfirm },
50+
props: { initialDate, initialTimeZone, onClose },
4951
});
5052

5153
await fireEvent.click(getConfirmButton());
5254

53-
expect(onConfirm).toHaveBeenCalledWith({ mode: 'absolute', date: '2024-01-01T00:00:00.000+01:00' });
55+
expect(onClose).toHaveBeenCalledWith({
56+
mode: 'absolute',
57+
date: '2024-01-01T00:00:00.000+01:00',
58+
dateTime: targetDate,
59+
});
5460
});
5561

5662
test('calls onCancel on cancel', async () => {
5763
render(ChangeDate, {
58-
props: { initialDate, initialTimeZone, onCancel, onConfirm },
64+
props: { initialDate, initialTimeZone, onClose },
5965
});
6066

6167
await fireEvent.click(getCancelButton());
6268

63-
expect(onCancel).toHaveBeenCalled();
69+
expect(onClose).toHaveBeenCalled();
6470
});
6571

6672
describe('when date is in daylight saving time', () => {
6773
const dstDate = DateTime.fromISO('2024-07-01');
68-
74+
const targetDate = DateTime.fromISO('2024-07-01').setZone('UTC+2', {
75+
keepLocalTime: true,
76+
});
6977
test('should render correct timezone with offset', () => {
70-
render(ChangeDate, { initialDate: dstDate, initialTimeZone, onCancel, onConfirm });
78+
render(ChangeDate, { initialDate: dstDate, initialTimeZone, onClose });
7179

7280
expect(getTimeZoneInput().value).toBe('Europe/Berlin (+02:00)');
7381
});
7482

7583
test('calls onConfirm with correct date on confirm', async () => {
7684
render(ChangeDate, {
77-
props: { initialDate: dstDate, initialTimeZone, onCancel, onConfirm },
85+
props: { initialDate: dstDate, initialTimeZone, onClose },
7886
});
7987

8088
await fireEvent.click(getConfirmButton());
8189

82-
expect(onConfirm).toHaveBeenCalledWith({ mode: 'absolute', date: '2024-07-01T00:00:00.000+02:00' });
90+
expect(onClose).toHaveBeenCalledWith({
91+
mode: 'absolute',
92+
date: '2024-07-01T00:00:00.000+02:00',
93+
dateTime: targetDate,
94+
});
8395
});
8496
});
8597

8698
test('calls onConfirm with correct offset in relative mode', async () => {
8799
render(ChangeDate, {
88-
props: { initialDate, initialTimeZone, currentInterval, onCancel, onConfirm },
100+
props: { initialDate, initialTimeZone, currentInterval, onClose },
89101
});
90102

91103
await fireEvent.click(getRelativeInputToggle());
@@ -104,7 +116,7 @@ describe('ChangeDate component', () => {
104116

105117
await fireEvent.click(getConfirmButton());
106118

107-
expect(onConfirm).toHaveBeenCalledWith({
119+
expect(onClose).toHaveBeenCalledWith({
108120
mode: 'relative',
109121
duration: days * 60 * 24 + hours * 60 + minutes,
110122
timeZone: undefined,
@@ -114,7 +126,7 @@ describe('ChangeDate component', () => {
114126
test('calls onConfirm with correct timeZone in relative mode', async () => {
115127
const user = userEvent.setup();
116128
render(ChangeDate, {
117-
props: { initialDate, initialTimeZone, currentInterval, onCancel, onConfirm },
129+
props: { initialDate, initialTimeZone, currentInterval, onClose },
118130
});
119131

120132
await user.click(getRelativeInputToggle());
@@ -123,7 +135,7 @@ describe('ChangeDate component', () => {
123135
await user.keyboard('{Enter}');
124136

125137
await user.click(getConfirmButton());
126-
expect(onConfirm).toHaveBeenCalledWith({
138+
expect(onClose).toHaveBeenCalledWith({
127139
mode: 'relative',
128140
duration: 0,
129141
timeZone: initialTimeZone,
@@ -177,7 +189,7 @@ describe('ChangeDate component', () => {
177189
];
178190

179191
const component = render(ChangeDate, {
180-
props: { initialDate, initialTimeZone, currentInterval, onCancel, onConfirm },
192+
props: { initialDate, initialTimeZone, currentInterval, onClose },
181193
});
182194

183195
for (const testCase of testCases) {

web/src/lib/components/shared-components/change-date.svelte

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<script lang="ts">
2+
import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
23
import DateInput from '$lib/elements/DateInput.svelte';
34
import DurationInput from '$lib/elements/DurationInput.svelte';
45
import { locale } from '$lib/stores/preferences.store';
5-
import { getDateTimeOffsetLocaleString } from '$lib/utils/timeline-util.js';
6-
import { ConfirmModal, Field, Switch } from '@immich/ui';
7-
import { mdiCalendarEditOutline } from '@mdi/js';
6+
import { getDateTimeOffsetLocaleString } from '$lib/utils/timeline-util';
7+
import { Button, Field, HStack, Modal, ModalBody, ModalFooter, Switch } from '@immich/ui';
8+
import { mdiCalendarEdit } from '@mdi/js';
89
import { DateTime, Duration } from 'luxon';
910
import { t } from 'svelte-i18n';
1011
import { get } from 'svelte/store';
11-
import Combobox, { type ComboBoxOption } from './combobox.svelte';
1212
1313
interface Props {
1414
title?: string;
@@ -17,8 +17,9 @@
1717
timezoneInput?: boolean;
1818
withDuration?: boolean;
1919
currentInterval?: { start: DateTime; end: DateTime };
20-
onCancel: () => void;
21-
onConfirm: (result: AbsoluteResult | RelativeResult) => void;
20+
icon?: string;
21+
confirmText?: string;
22+
onClose: (result?: AbsoluteResult | RelativeResult) => void;
2223
}
2324
2425
let {
@@ -28,13 +29,15 @@
2829
timezoneInput = true,
2930
withDuration = true,
3031
currentInterval = undefined,
31-
onCancel,
32-
onConfirm,
32+
icon = mdiCalendarEdit,
33+
confirmText = $t('confirm'),
34+
onClose,
3335
}: Props = $props();
3436
3537
export type AbsoluteResult = {
3638
mode: 'absolute';
3739
date: string;
40+
dateTime: DateTime<true>;
3841
};
3942
4043
export type RelativeResult = {
@@ -191,14 +194,23 @@
191194
const fixedOffsetZone = `UTC${offsetMinutes >= 0 ? '+' : ''}${Duration.fromObject({ minutes: offsetMinutes }).toFormat('hh:mm')}`;
192195
193196
// Create a DateTime object in this fixed-offset zone, preserving the local time.
194-
const finalDateTime = DateTime.fromObject(dtComponents.toObject(), { zone: fixedOffsetZone });
195-
196-
onConfirm({ mode: 'absolute', date: finalDateTime.toISO({ includeOffset: true })! });
197+
const fixedOffsetDateTime = DateTime.fromObject(dtComponents.toObject(), {
198+
zone: fixedOffsetZone,
199+
}) as DateTime<true>;
200+
201+
onClose({
202+
mode: 'absolute',
203+
date: fixedOffsetDateTime.toISO({ includeOffset: true })!,
204+
dateTime: fixedOffsetDateTime,
205+
});
206+
return;
197207
}
198208
199209
if (showRelative && (selectedDuration || selectedRelativeOption)) {
200-
onConfirm({ mode: 'relative', duration: selectedDuration, timeZone: selectedRelativeOption?.value });
210+
onClose({ mode: 'relative', duration: selectedDuration, timeZone: selectedRelativeOption?.value });
211+
return;
201212
}
213+
onClose();
202214
};
203215
204216
const handleOnSelect = (option?: ComboBoxOption) => {
@@ -234,15 +246,8 @@
234246
);
235247
</script>
236248

237-
<ConfirmModal
238-
confirmColor="primary"
239-
{title}
240-
icon={mdiCalendarEditOutline}
241-
prompt="Please select a new date:"
242-
disabled={!date.isValid}
243-
onClose={(confirmed) => (confirmed ? handleConfirm() : onCancel())}
244-
>
245-
{#snippet promptSnippet()}
249+
<Modal {title} {icon} {onClose}>
250+
<ModalBody>
246251
{#if withDuration}
247252
<div class="mb-5">
248253
<Field label={$t('edit_date_and_time_by_offset')}>
@@ -280,5 +285,15 @@
280285
</div>
281286
</div>
282287
</div>
283-
{/snippet}
284-
</ConfirmModal>
288+
</ModalBody>
289+
<ModalFooter>
290+
<HStack fullWidth>
291+
<Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>
292+
{$t('cancel')}
293+
</Button>
294+
<Button shape="round" color="primary" fullWidth onclick={handleConfirm} disabled={!date.isValid}>
295+
{confirmText}
296+
</Button>
297+
</HStack>
298+
</ModalFooter>
299+
</Modal>

0 commit comments

Comments
 (0)