From 2919ee4c65c6962a9956c6a5a33cbb533fe430ea Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Thu, 16 Oct 2025 13:44:09 -0400 Subject: [PATCH] fix: navigate to time action (#20928) * fix: navigate to time action * change-date -> DateSelectionModal; use luxon; use handle* for callback fn name * refactor change-date dialogs * Review comments * chore: clean up --------- Co-authored-by: Jason Rasmussen --- i18n/en.json | 3 + .../asset-viewer/detail-panel.svelte | 43 +-- .../shared-components/change-date.svelte | 284 ------------------ .../timeline/actions/ChangeDateAction.svelte | 80 +---- .../actions/ChangeDescriptionAction.svelte | 4 +- .../actions/ChangeLocationAction.svelte | 4 +- .../actions/TimelineKeyboardActions.svelte | 39 +-- web/src/lib/elements/DateInput.svelte | 4 +- .../internal/search-support.svelte.spec.ts | 75 +++++ .../internal/search-support.svelte.ts | 20 ++ .../timeline-manager.svelte.ts | 9 +- .../lib/modals/AssetChangeDateModal.svelte | 84 ++++++ .../AssetSelectionChangeDateModal.spec.ts} | 120 ++++---- .../AssetSelectionChangeDateModal.svelte | 136 +++++++++ web/src/lib/modals/NavigateToDateModal.svelte | 61 ++++ web/src/lib/modals/ShortcutsModal.svelte | 1 + web/src/lib/modals/timezone-utils.ts | 149 +++++++++ web/src/lib/utils/asset-utils.ts | 2 +- web/src/lib/utils/timeline-util.ts | 6 - 19 files changed, 647 insertions(+), 477 deletions(-) delete mode 100644 web/src/lib/components/shared-components/change-date.svelte create mode 100644 web/src/lib/managers/timeline-manager/internal/search-support.svelte.spec.ts create mode 100644 web/src/lib/modals/AssetChangeDateModal.svelte rename web/src/lib/{components/shared-components/change-date.spec.ts => modals/AssetSelectionChangeDateModal.spec.ts} (61%) create mode 100644 web/src/lib/modals/AssetSelectionChangeDateModal.svelte create mode 100644 web/src/lib/modals/NavigateToDateModal.svelte create mode 100644 web/src/lib/modals/timezone-utils.ts diff --git a/i18n/en.json b/i18n/en.json index 0d9e52681c..e1702611a7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1364,6 +1364,8 @@ "my_albums": "My albums", "name": "Name", "name_or_nickname": "Name or nickname", + "navigate": "Navigate", + "navigate_to_time": "Navigate to Time", "network_requirement_photos_upload": "Use cellular data to backup photos", "network_requirement_videos_upload": "Use cellular data to backup videos", "network_requirements": "Network Requirements", @@ -1373,6 +1375,7 @@ "never": "Never", "new_album": "New Album", "new_api_key": "New API Key", + "new_date_range": "New date range", "new_password": "New password", "new_person": "New person", "new_pin_code": "New PIN code", diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 4361da207b..17cba69d99 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -5,12 +5,9 @@ import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte'; import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte'; import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte'; - import ChangeDate, { - type AbsoluteResult, - type RelativeResult, - } from '$lib/components/shared-components/change-date.svelte'; import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants'; import { authManager } from '$lib/managers/auth-manager.svelte'; + import AssetChangeDateModal from '$lib/modals/AssetChangeDateModal.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { boundingBoxesArray } from '$lib/stores/people.store'; import { locale } from '$lib/stores/preferences.store'; @@ -19,12 +16,11 @@ import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils'; import { delay, getDimensions } from '$lib/utils/asset-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; - import { handleError } from '$lib/utils/handle-error'; import { getMetadataSearchQuery } from '$lib/utils/metadata-search'; - import { fromISODateTime, fromISODateTimeUTC } from '$lib/utils/timeline-util'; + import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util'; import { getParentPath } from '$lib/utils/tree-utils'; - import { AssetMediaSize, getAssetInfo, updateAsset, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk'; - import { Icon, IconButton, LoadingSpinner } from '@immich/ui'; + import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk'; + import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui'; import { mdiCalendar, mdiCameraIris, @@ -59,7 +55,7 @@ let people = $derived(asset.people || []); let unassignedFaces = $derived(asset.unassignedFaces || []); let showingHiddenPeople = $state(false); - let timeZone = $derived(asset.exifInfo?.timeZone); + let timeZone = $derived(asset.exifInfo?.timeZone ?? undefined); let dateTime = $derived( timeZone && asset.exifInfo?.dateTimeOriginal ? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone) @@ -112,18 +108,13 @@ const toggleAssetPath = () => (showAssetPath = !showAssetPath); - let isShowChangeDate = $state(false); - - async function handleConfirmChangeDate(result: AbsoluteResult | RelativeResult) { - isShowChangeDate = false; - try { - if (result.mode === 'absolute') { - await updateAsset({ id: asset.id, updateAssetDto: { dateTimeOriginal: result.date } }); - } - } catch (error) { - handleError(error, $t('errors.unable_to_change_date')); + const handleChangeDate = async () => { + if (!isOwner) { + return; } - } + + await modalManager.show(AssetChangeDateModal, { asset: toTimelineAsset(asset), initialDate: dateTime }); + };
@@ -280,7 +271,7 @@ + + + + diff --git a/web/src/lib/components/shared-components/change-date.spec.ts b/web/src/lib/modals/AssetSelectionChangeDateModal.spec.ts similarity index 61% rename from web/src/lib/components/shared-components/change-date.spec.ts rename to web/src/lib/modals/AssetSelectionChangeDateModal.spec.ts index 63926a44a6..ab7f24db25 100644 --- a/web/src/lib/components/shared-components/change-date.spec.ts +++ b/web/src/lib/modals/AssetSelectionChangeDateModal.spec.ts @@ -1,33 +1,30 @@ +import { getAnimateMock } from '$lib/__mocks__/animate.mock'; import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock'; +import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { getVisualViewportMock } from '$lib/__mocks__/visual-viewport.mock'; +import { calcNewDate } from '$lib/modals/timezone-utils'; import { fireEvent, render, screen, waitFor } from '@testing-library/svelte'; import userEvent from '@testing-library/user-event'; import { DateTime } from 'luxon'; -import ChangeDate from './change-date.svelte'; +import AssetSelectionChangeDateModal from './AssetSelectionChangeDateModal.svelte'; -describe('ChangeDate component', () => { +describe('DateSelectionModal component', () => { const initialDate = DateTime.fromISO('2024-01-01'); const initialTimeZone = 'Europe/Berlin'; - const currentInterval = { - start: DateTime.fromISO('2000-02-01T14:00:00+01:00'), - end: DateTime.fromISO('2001-02-01T14:00:00+01:00'), - }; - const onCancel = vi.fn(); - const onConfirm = vi.fn(); + + const onClose = vi.fn(); const getRelativeInputToggle = () => screen.getByTestId('edit-by-offset-switch'); const getDateInput = () => screen.getByLabelText('date_and_time') as HTMLInputElement; const getTimeZoneInput = () => screen.getByLabelText('timezone') as HTMLInputElement; - const getCancelButton = () => screen.getByText('Cancel'); - const getConfirmButton = () => screen.getByText('Confirm'); + const getCancelButton = () => screen.getByText('cancel'); + const getConfirmButton = () => screen.getByText('confirm'); beforeEach(() => { vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock()); vi.stubGlobal('visualViewport', getVisualViewportMock()); - }); - - afterEach(() => { vi.resetAllMocks(); + Element.prototype.animate = getAnimateMock(); }); afterAll(async () => { @@ -38,54 +35,75 @@ describe('ChangeDate component', () => { }); test('should render correct values', () => { - render(ChangeDate, { initialDate, initialTimeZone, onCancel, onConfirm }); + render(AssetSelectionChangeDateModal, { + initialDate, + initialTimeZone, + assets: [], + + onClose, + }); expect(getDateInput().value).toBe('2024-01-01T00:00'); expect(getTimeZoneInput().value).toBe('Europe/Berlin (+01:00)'); }); test('calls onConfirm with correct date on confirm', async () => { - render(ChangeDate, { - props: { initialDate, initialTimeZone, onCancel, onConfirm }, + render(AssetSelectionChangeDateModal, { + props: { initialDate, initialTimeZone, assets: [], onClose }, }); await fireEvent.click(getConfirmButton()); - expect(onConfirm).toHaveBeenCalledWith({ mode: 'absolute', date: '2024-01-01T00:00:00.000+01:00' }); + expect(sdkMock.updateAssets).toHaveBeenCalledWith({ + assetBulkUpdateDto: { + ids: [], + dateTimeOriginal: '2024-01-01T00:00:00.000+01:00', + }, + }); }); test('calls onCancel on cancel', async () => { - render(ChangeDate, { - props: { initialDate, initialTimeZone, onCancel, onConfirm }, + render(AssetSelectionChangeDateModal, { + props: { initialDate, initialTimeZone, assets: [], onClose }, }); await fireEvent.click(getCancelButton()); - expect(onCancel).toHaveBeenCalled(); + expect(onClose).toHaveBeenCalled(); }); describe('when date is in daylight saving time', () => { const dstDate = DateTime.fromISO('2024-07-01'); test('should render correct timezone with offset', () => { - render(ChangeDate, { initialDate: dstDate, initialTimeZone, onCancel, onConfirm }); + render(AssetSelectionChangeDateModal, { + initialDate: dstDate, + initialTimeZone, + assets: [], + onClose, + }); expect(getTimeZoneInput().value).toBe('Europe/Berlin (+02:00)'); }); test('calls onConfirm with correct date on confirm', async () => { - render(ChangeDate, { - props: { initialDate: dstDate, initialTimeZone, onCancel, onConfirm }, + render(AssetSelectionChangeDateModal, { + props: { initialDate: dstDate, initialTimeZone, assets: [], onClose }, }); await fireEvent.click(getConfirmButton()); - expect(onConfirm).toHaveBeenCalledWith({ mode: 'absolute', date: '2024-07-01T00:00:00.000+02:00' }); + expect(sdkMock.updateAssets).toHaveBeenCalledWith({ + assetBulkUpdateDto: { + ids: [], + dateTimeOriginal: '2024-07-01T00:00:00.000+02:00', + }, + }); }); }); test('calls onConfirm with correct offset in relative mode', async () => { - render(ChangeDate, { - props: { initialDate, initialTimeZone, currentInterval, onCancel, onConfirm }, + render(AssetSelectionChangeDateModal, { + props: { initialDate, initialTimeZone, assets: [], onClose }, }); await fireEvent.click(getRelativeInputToggle()); @@ -104,17 +122,19 @@ describe('ChangeDate component', () => { await fireEvent.click(getConfirmButton()); - expect(onConfirm).toHaveBeenCalledWith({ - mode: 'relative', - duration: days * 60 * 24 + hours * 60 + minutes, - timeZone: undefined, + expect(sdkMock.updateAssets).toHaveBeenCalledWith({ + assetBulkUpdateDto: { + ids: [], + dateTimeRelative: days * 60 * 24 + hours * 60 + minutes, + timeZone: 'Europe/Berlin', + }, }); }); test('calls onConfirm with correct timeZone in relative mode', async () => { const user = userEvent.setup(); - render(ChangeDate, { - props: { initialDate, initialTimeZone, currentInterval, onCancel, onConfirm }, + render(AssetSelectionChangeDateModal, { + props: { initialDate, initialTimeZone, assets: [], onClose }, }); await user.click(getRelativeInputToggle()); @@ -123,10 +143,13 @@ describe('ChangeDate component', () => { await user.keyboard('{Enter}'); await user.click(getConfirmButton()); - expect(onConfirm).toHaveBeenCalledWith({ - mode: 'relative', - duration: 0, - timeZone: initialTimeZone, + + expect(sdkMock.updateAssets).toHaveBeenCalledWith({ + assetBulkUpdateDto: { + ids: [], + dateTimeRelative: 0, + timeZone: 'Europe/Berlin', + }, }); }); @@ -136,55 +159,50 @@ describe('ChangeDate component', () => { timestamp: DateTime.fromISO('2024-01-01T00:00:00.000+01:00', { setZone: true }), duration: 0, timezone: undefined, - expectedResult: 'Jan 1, 2024, 12:00 AM GMT+01:00', + expectedResult: '2024-01-01T00:00:00.000', }, { timestamp: DateTime.fromISO('2024-01-01T04:00:00.000+05:00', { setZone: true }), duration: 0, timezone: undefined, - expectedResult: 'Jan 1, 2024, 4:00 AM GMT+05:00', + expectedResult: '2024-01-01T04:00:00.000', }, { timestamp: DateTime.fromISO('2024-01-01T00:00:00.000+00:00', { setZone: true }), duration: 0, timezone: 'Europe/Berlin', - expectedResult: 'Jan 1, 2024, 1:00 AM GMT+01:00', + expectedResult: '2024-01-01T01:00:00.000', }, { timestamp: DateTime.fromISO('2024-07-01T00:00:00.000+00:00', { setZone: true }), duration: 0, timezone: 'Europe/Berlin', - expectedResult: 'Jul 1, 2024, 2:00 AM GMT+02:00', + expectedResult: '2024-07-01T02:00:00.000', }, { timestamp: DateTime.fromISO('2024-01-01T00:00:00.000+01:00', { setZone: true }), duration: 1440, timezone: undefined, - expectedResult: 'Jan 2, 2024, 12:00 AM GMT+01:00', + expectedResult: '2024-01-02T00:00:00.000', }, { timestamp: DateTime.fromISO('2024-01-01T00:00:00.000+01:00', { setZone: true }), duration: -1440, timezone: undefined, - expectedResult: 'Dec 31, 2023, 12:00 AM GMT+01:00', + expectedResult: '2023-12-31T00:00:00.000', }, { timestamp: DateTime.fromISO('2024-01-01T00:00:00.000-01:00', { setZone: true }), duration: -1440, timezone: 'America/Anchorage', - expectedResult: 'Dec 30, 2023, 4:00 PM GMT-09:00', + expectedResult: '2023-12-30T16:00:00.000', }, ]; - const component = render(ChangeDate, { - props: { initialDate, initialTimeZone, currentInterval, onCancel, onConfirm }, - }); - for (const testCase of testCases) { - expect( - component.component.calcNewDate(testCase.timestamp, testCase.duration, testCase.timezone), - JSON.stringify(testCase), - ).toBe(testCase.expectedResult); + expect(calcNewDate(testCase.timestamp, testCase.duration, testCase.timezone), JSON.stringify(testCase)).toBe( + testCase.expectedResult, + ); } }); }); diff --git a/web/src/lib/modals/AssetSelectionChangeDateModal.svelte b/web/src/lib/modals/AssetSelectionChangeDateModal.svelte new file mode 100644 index 0000000000..ae9bfeb1d0 --- /dev/null +++ b/web/src/lib/modals/AssetSelectionChangeDateModal.svelte @@ -0,0 +1,136 @@ + + + onClose(false)} size="small"> + + + + + + + + {#if showRelative} + + + + + + + {:else} + + + + + + + {/if} +
+ (lastSelectedTimezone = option as ZoneOption)} + > +
+ +
+
+ + + + + + +
diff --git a/web/src/lib/modals/NavigateToDateModal.svelte b/web/src/lib/modals/NavigateToDateModal.svelte new file mode 100644 index 0000000000..4b83c66bc6 --- /dev/null +++ b/web/src/lib/modals/NavigateToDateModal.svelte @@ -0,0 +1,61 @@ + + + onClose()}> + + + + + + + + + + + + + + + + + diff --git a/web/src/lib/modals/ShortcutsModal.svelte b/web/src/lib/modals/ShortcutsModal.svelte index 9bd7b29b94..ebb5ea3c60 100644 --- a/web/src/lib/modals/ShortcutsModal.svelte +++ b/web/src/lib/modals/ShortcutsModal.svelte @@ -27,6 +27,7 @@ { key: ['D', 'd'], action: $t('previous_or_next_day') }, { key: ['M', 'm'], action: $t('previous_or_next_month') }, { key: ['Y', 'y'], action: $t('previous_or_next_year') }, + { key: ['g'], action: $t('navigate_to_time') }, { key: ['x'], action: $t('select') }, { key: ['Esc'], action: $t('back_close_deselect') }, { key: ['Ctrl', 'k'], action: $t('search_your_photos') }, diff --git a/web/src/lib/modals/timezone-utils.ts b/web/src/lib/modals/timezone-utils.ts new file mode 100644 index 0000000000..c7bb00fd69 --- /dev/null +++ b/web/src/lib/modals/timezone-utils.ts @@ -0,0 +1,149 @@ +import { DateTime, Duration } from 'luxon'; + +export type ZoneOption = { + /** + * Timezone name with offset + * + * e.g. Asia/Jerusalem (+03:00) + */ + label: string; + + /** + * Timezone name + * + * e.g. Asia/Jerusalem + */ + value: string; + + /** + * Timezone offset in minutes + * + * e.g. 300 + */ + offsetMinutes: number; + + /** + * True iff the date is valid + * + * Dates may be invalid for various reasons, for example setting a day that does not exist (30 Feb 2024). + * Due to daylight saving time, 2:30am is invalid for Europe/Berlin on Mar 31 2024.The two following local times + * are one second apart: + * + * - Mar 31 2024 01:59:59 (GMT+0100, unix timestamp 1725058799) + * - Mar 31 2024 03:00:00 (GMT+0200, unix timestamp 1711846800) + * + * Mar 31 2024 02:30:00 does not exist in Europe/Berlin, this is an invalid date/time/time zone combination. + */ + valid: boolean; +}; + +const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; +const knownTimezones = Intl.supportedValuesOf('timeZone'); + +export function getTimezones(selectedDate: string) { + // Use a fixed modern date to calculate stable timezone offsets for the list + // This ensures that the offsets shown in the combobox are always current, + // regardless of the historical date selected by the user. + return knownTimezones + .map((zone) => zoneOptionForDate(zone, selectedDate)) + .filter((zone) => zone.valid) + .sort((zoneA, zoneB) => sortTwoZones(zoneA, zoneB)); +} + +export function getModernOffsetForZoneAndDate( + zone: string, + dateString: string, +): { offsetMinutes: number; offsetFormat: string } { + const dt = DateTime.fromISO(dateString, { zone }); + + // we determine the *modern* offset for this zone based on its current rules. + // To do this, we "move" the date to the current year, keeping the local time components. + // This allows Luxon to apply current-year DST rules. + const modernYearDt = dt.set({ year: DateTime.now().year }); + + // Calculate the offset at that modern year's date. + const modernOffsetMinutes = modernYearDt.setZone(zone, { keepLocalTime: true }).offset; + const modernOffsetFormat = modernYearDt.setZone(zone, { keepLocalTime: true }).toFormat('ZZ'); + + return { offsetMinutes: modernOffsetMinutes, offsetFormat: modernOffsetFormat }; +} + +function zoneOptionForDate(zone: string, date: string) { + const { offsetMinutes, offsetFormat: zoneOffsetAtDate } = getModernOffsetForZoneAndDate(zone, date); + // For validity, we still need to check if the exact date/time exists in the *original* timezone (for gaps/overlaps). + const dateForValidity = DateTime.fromISO(date, { zone }); + const valid = dateForValidity.isValid && date === dateForValidity.toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + return { + value: zone, + offsetMinutes, + label: zone + ' (' + zoneOffsetAtDate + ')' + (valid ? '' : ' [invalid date!]'), + valid, + }; +} + +function sortTwoZones(zoneA: ZoneOption, zoneB: ZoneOption) { + const offsetDifference = zoneA.offsetMinutes - zoneB.offsetMinutes; + if (offsetDifference != 0) { + return offsetDifference; + } + return zoneA.value.localeCompare(zoneB.value, undefined, { sensitivity: 'base' }); +} + +/* + * If the time zone is not given, find the timezone to select for a given time, date, and offset (e.g. +02:00). + * + * This is done so that the list shown to the user includes more helpful names like "Europe/Berlin (+02:00)" + * instead of just the raw offset or something like "UTC+02:00". + * + * The provided information (initialDate, from some asset) includes the offset (e.g. +02:00), but no information about + * the actual time zone. As several countries/regions may share the same offset, for example Berlin (Germany) and + * Blantyre (Malawi) sharing +02:00 in summer, we have to guess and somehow pick a suitable time zone. + * + * If the time zone configured by the user (in the browser) provides the same offset for the given date (accounting + * for daylight saving time and other weirdness), we prefer to show it. This way, for German users, we might be able + * to show "Europe/Berlin" instead of the lexicographically first entry "Africa/Blantyre". + */ +export function getPreferredTimeZone( + date: DateTime, + initialTimeZone: string | undefined, + timezones: ZoneOption[], + selectedOption?: ZoneOption, +) { + const offset = date.offset; + const previousSelection = timezones.find((item) => item.value === selectedOption?.value); + const fromInitialTimeZone = timezones.find((item) => item.value === initialTimeZone); + const sameAsUserTimeZone = timezones.find((item) => item.offsetMinutes === offset && item.value === userTimeZone); + const firstWithSameOffset = timezones.find((item) => item.offsetMinutes === offset); + const utcFallback = { + label: 'UTC (+00:00)', + offsetMinutes: 0, + value: 'UTC', + valid: true, + }; + return previousSelection ?? fromInitialTimeZone ?? sameAsUserTimeZone ?? firstWithSameOffset ?? utcFallback; +} + +export function toDatetime(selectedDate: string, selectedZone: ZoneOption) { + const dtComponents = DateTime.fromISO(selectedDate, { zone: 'utc' }); + + // Determine the modern, DST-aware offset for the selected IANA zone + const { offsetMinutes } = getModernOffsetForZoneAndDate(selectedZone.value, selectedDate); + + // Construct the final ISO string with a fixed-offset zone. + const fixedOffsetZone = `UTC${offsetMinutes >= 0 ? '+' : ''}${Duration.fromObject({ minutes: offsetMinutes }).toFormat('hh:mm')}`; + + // Create a DateTime object in this fixed-offset zone, preserving the local time. + return DateTime.fromObject(dtComponents.toObject(), { zone: fixedOffsetZone }); +} + +export function toIsoDate(selectedDate: string, selectedZone: ZoneOption) { + return toDatetime(selectedDate, selectedZone).toISO({ includeOffset: true })!; +} + +export const calcNewDate = (timestamp: DateTime, selectedDuration: number, timezone?: string) => { + let newDateTime = timestamp.plus({ minutes: selectedDuration }); + if (timezone) { + newDateTime = newDateTime.setZone(timezone); + } + return newDateTime.toFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); +}; diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 25e045c8a1..5211f0bf72 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -405,7 +405,7 @@ export const getAssetType = (type: AssetTypeEnum) => { } }; -export const getSelectedAssets = (assets: TimelineAsset[], user: UserResponseDto | null): string[] => { +export const getOwnedAssetsWithWarning = (assets: TimelineAsset[], user: UserResponseDto | null): string[] => { const ids = [...assets].filter((a) => user && a.ownerId === user.id).map((a) => a.id); const numberOfIssues = [...assets].filter((a) => user && a.ownerId !== user.id).length; diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 673b929a1c..60811c24f0 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -154,12 +154,6 @@ export function formatGroupTitle(_date: DateTime): string { export const getDateLocaleString = (date: DateTime, opts?: LocaleOptions): string => date.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY, opts); -export const getDateTimeOffsetLocaleString = (date: DateTime, opts?: LocaleOptions): string => - date.toLocaleString( - { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'longOffset' }, - opts, - ); - export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): TimelineAsset => { if (isTimelineAsset(unknownAsset)) { return unknownAsset;