mirror of
https://github.com/immich-app/immich.git
synced 2026-03-04 09:57:33 +03:00
feat(web): change link expiration logic & presets (#26064)
* feat(web): link expiration presets * refactor: implement suggestions * chore: remove createdAt prop * fix: tests * fix: button keys
This commit is contained in:
@@ -1,21 +1,16 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import { Button, DatePicker, Field } from '@immich/ui';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { minBy, uniqBy } from 'lodash-es';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
createdAt?: string;
|
||||
expiresAt: string | null;
|
||||
};
|
||||
|
||||
let { createdAt = DateTime.now().toISO(), expiresAt = $bindable() }: Props = $props();
|
||||
let { expiresAt = $bindable() }: Props = $props();
|
||||
|
||||
const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
|
||||
[30, 'minutes'],
|
||||
[1, 'hour'],
|
||||
[6, 'hours'],
|
||||
[1, 'day'],
|
||||
[7, 'days'],
|
||||
[30, 'days'],
|
||||
@@ -26,50 +21,52 @@
|
||||
const relativeTime = $derived(new Intl.RelativeTimeFormat($locale));
|
||||
const expiredDateOptions = $derived([
|
||||
{ text: $t('never'), value: 0 },
|
||||
...expirationOptions
|
||||
.map(([value, unit]) => ({
|
||||
text: relativeTime.format(value, unit),
|
||||
value: Duration.fromObject({ [unit]: value }).toMillis(),
|
||||
}))
|
||||
.filter(({ value: millis }) => DateTime.fromISO(createdAt).plus(millis) > DateTime.now()),
|
||||
...expirationOptions.map(([value, unit]) => ({
|
||||
text: relativeTime.format(value, unit),
|
||||
value: Duration.fromObject({ [unit]: value }).toMillis(),
|
||||
})),
|
||||
]);
|
||||
|
||||
const getExpirationOption = (createdAt: string, expiresAt: string | null) => {
|
||||
if (!expiresAt) {
|
||||
return expiredDateOptions[0];
|
||||
}
|
||||
let selectedPresetValue = $state<number | null>(null);
|
||||
|
||||
const delta = DateTime.fromISO(expiresAt).diff(DateTime.fromISO(createdAt)).toMillis();
|
||||
const closestOption = minBy(expiredDateOptions, ({ value }) => Math.abs(delta - value));
|
||||
|
||||
if (!closestOption) {
|
||||
return expiredDateOptions[0];
|
||||
}
|
||||
|
||||
// allow a generous epsilon to compensate for potential API delays
|
||||
if (Math.abs(closestOption.value - delta) > 10_000) {
|
||||
const interval = DateTime.fromMillis(closestOption.value) as DateTime<true>;
|
||||
return { text: interval.toRelative({ locale: $locale }), value: closestOption.value };
|
||||
}
|
||||
|
||||
return closestOption;
|
||||
const getSelectedDate = (): DateTime | undefined => {
|
||||
return expiresAt ? DateTime.fromISO(expiresAt) : undefined;
|
||||
};
|
||||
|
||||
const onSelect = (option: number | string) => {
|
||||
const expirationOption = Number(option);
|
||||
|
||||
expiresAt = expirationOption === 0 ? null : DateTime.fromISO(createdAt).plus(expirationOption).toISO();
|
||||
const setSelectedDate = (value: DateTime | undefined) => {
|
||||
selectedPresetValue = null; // Clear preset when manually setting date
|
||||
expiresAt = value ? value.toISO() : null;
|
||||
};
|
||||
|
||||
let expirationOption = $derived(getExpirationOption(createdAt, expiresAt).value);
|
||||
const selectPreset = (value: number) => {
|
||||
selectedPresetValue = value;
|
||||
if (value === 0) {
|
||||
expiresAt = null;
|
||||
return;
|
||||
}
|
||||
const newDate = DateTime.now().plus(value);
|
||||
expiresAt = newDate.toISO();
|
||||
};
|
||||
|
||||
const isSelected = (value: number) => {
|
||||
return selectedPresetValue === value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mt-2">
|
||||
<SettingSelect
|
||||
bind:value={expirationOption}
|
||||
{onSelect}
|
||||
options={uniqBy([...expiredDateOptions, getExpirationOption(createdAt, expiresAt)], 'value')}
|
||||
label={$t('expire_after')}
|
||||
number={true}
|
||||
/>
|
||||
<Field label={$t('expire_after')}>
|
||||
<DatePicker bind:value={getSelectedDate, setSelectedDate} />
|
||||
</Field>
|
||||
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
{#each expiredDateOptions as option (option.value)}
|
||||
<Button
|
||||
size="tiny"
|
||||
variant={isSelected(option.value) ? 'filled' : 'outline'}
|
||||
onclick={() => selectPreset(option.value)}
|
||||
>
|
||||
{option.text}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render } from '@testing-library/svelte';
|
||||
import { renderWithTooltips } from '$tests/helpers';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SharedLinkFormFields from './SharedLinkFormFields.svelte';
|
||||
|
||||
@@ -7,16 +7,14 @@ describe('SharedLinkFormFields component', () => {
|
||||
element instanceof HTMLInputElement ? element.checked : element.getAttribute('aria-checked') === 'true';
|
||||
|
||||
it('turns downloads off when metadata is disabled', async () => {
|
||||
const { container } = render(SharedLinkFormFields, {
|
||||
props: {
|
||||
slug: '',
|
||||
password: '',
|
||||
description: '',
|
||||
allowDownload: true,
|
||||
allowUpload: false,
|
||||
showMetadata: true,
|
||||
expiresAt: null,
|
||||
},
|
||||
const { container } = renderWithTooltips(SharedLinkFormFields, {
|
||||
slug: '',
|
||||
password: '',
|
||||
description: '',
|
||||
allowDownload: true,
|
||||
allowUpload: false,
|
||||
showMetadata: true,
|
||||
expiresAt: null,
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
allowUpload: boolean;
|
||||
showMetadata: boolean;
|
||||
expiresAt: string | null;
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -22,7 +21,6 @@
|
||||
allowUpload = $bindable(),
|
||||
showMetadata = $bindable(),
|
||||
expiresAt = $bindable(),
|
||||
createdAt,
|
||||
}: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
@@ -50,7 +48,7 @@
|
||||
<Input bind:value={description} autocomplete="off" />
|
||||
</Field>
|
||||
|
||||
<SharedLinkExpiration {createdAt} bind:expiresAt />
|
||||
<SharedLinkExpiration bind:expiresAt />
|
||||
<Field label={$t('show_metadata')}>
|
||||
<Switch bind:checked={showMetadata} />
|
||||
</Field>
|
||||
|
||||
Reference in New Issue
Block a user