feat: shared link edit (#24783)

This commit is contained in:
Jason Rasmussen
2025-12-22 11:47:06 -05:00
committed by GitHub
parent 952f189d8b
commit 1c156a179b
15 changed files with 202 additions and 118 deletions
@@ -1,98 +0,0 @@
<script lang="ts">
import SharedLinkExpiration from '$lib/components/SharedLinkExpiration.svelte';
import { handleUpdateSharedLink } from '$lib/services/shared-link.service';
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, PasswordInput, Switch, Text } from '@immich/ui';
import { mdiLink } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
onClose: (success?: boolean) => void;
sharedLink: SharedLinkResponseDto;
}
let { onClose, sharedLink }: Props = $props();
let description = $state(sharedLink.description ?? '');
let allowDownload = $state(sharedLink.allowDownload);
let allowUpload = $state(sharedLink.allowUpload);
let showMetadata = $state(sharedLink.showMetadata);
let password = $state(sharedLink.password ?? '');
let slug = $state(sharedLink.slug ?? '');
let shareType = sharedLink.album ? SharedLinkType.Album : SharedLinkType.Individual;
let expiresAt = $state(sharedLink.expiresAt);
const onUpdate = async () => {
const success = await handleUpdateSharedLink(sharedLink, {
description,
password: password ?? null,
expiresAt,
allowUpload,
allowDownload,
showMetadata,
slug: slug.trim() ?? null,
});
if (success) {
onClose(true);
}
};
</script>
<Modal title={$t('edit_link')} icon={mdiLink} size="small" {onClose}>
<ModalBody>
{#if shareType === SharedLinkType.Album}
<div class="text-sm">
{$t('public_album')} |
<span class="text-primary">{sharedLink.album?.albumName}</span>
</div>
{/if}
{#if shareType === SharedLinkType.Individual}
<div class="text-sm">
{$t('individual_share')} |
<span class="text-primary">{sharedLink.description || ''}</span>
</div>
{/if}
<div class="flex flex-col gap-4 mt-4">
<div>
<Field label={$t('custom_url')} description={$t('shared_link_custom_url_description')}>
<Input bind:value={slug} autocomplete="off" />
</Field>
{#if slug}
<Text size="tiny" color="muted" class="pt-2">/s/{encodeURIComponent(slug)}</Text>
{/if}
</div>
<Field label={$t('password')} description={$t('shared_link_password_description')}>
<PasswordInput bind:value={password} autocomplete="new-password" />
</Field>
<Field label={$t('description')}>
<Input bind:value={description} autocomplete="off" />
</Field>
<SharedLinkExpiration createdAt={sharedLink.createdAt} bind:expiresAt />
<Field label={$t('show_metadata')}>
<Switch bind:checked={showMetadata} />
</Field>
<Field label={$t('allow_public_user_to_download')} disabled={!showMetadata}>
<Switch bind:checked={allowDownload} />
</Field>
<Field label={$t('allow_public_user_to_upload')}>
<Switch bind:checked={allowUpload} />
</Field>
</div>
</ModalBody>
<ModalFooter>
<HStack fullWidth>
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button fullWidth shape="round" onclick={onUpdate}>{$t('confirm')}</Button>
</HStack>
</ModalFooter>
</Modal>
+1 -1
View File
@@ -24,7 +24,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
const Edit: ActionItem = {
title: $t('edit_link'),
icon: mdiPencilOutline,
onAction: () => goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
onAction: () => goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}/edit`),
};
const Delete: ActionItem = {
@@ -6,20 +6,20 @@
import SharedLinkCard from '$lib/components/sharedlinks-page/SharedLinkCard.svelte';
import { AppRoute } from '$lib/constants';
import GroupTab from '$lib/elements/GroupTab.svelte';
import SharedLinkUpdateModal from '$lib/modals/SharedLinkUpdateModal.svelte';
import { getAllSharedLinks, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
import { onMount } from 'svelte';
import { Container } from '@immich/ui';
import { onMount, type Snippet } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import type { LayoutData } from './$types';
type Props = {
data: PageData;
children?: Snippet;
data: LayoutData;
};
const { data }: Props = $props();
const { children, data }: Props = $props();
let sharedLinks: SharedLinkResponseDto[] = $state([]);
let sharedLink = $derived(sharedLinks.find(({ id }) => id === page.params.id));
const refresh = async () => {
sharedLinks = await getAllSharedLinks({});
@@ -80,7 +80,7 @@
</div>
{/snippet}
<div class="w-full max-w-3xl m-auto">
<Container center size="medium">
{#if sharedLinks.length === 0}
<div
class="flex place-content-center place-items-center rounded-lg bg-gray-100 dark:bg-immich-dark-gray dark:text-immich-gray p-12"
@@ -95,8 +95,6 @@
</div>
{/if}
{#if sharedLink}
<SharedLinkUpdateModal {sharedLink} onClose={() => goto(AppRoute.SHARED_LINKS)} />
{/if}
</div>
{@render children?.()}
</Container>
</UserPageLayout>
@@ -0,0 +1,14 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { LayoutLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url);
const $t = await getFormatter();
return {
meta: {
title: $t('shared_links'),
},
};
}) satisfies LayoutLoad;
@@ -0,0 +1,28 @@
import { AppRoute, UUID_REGEX } from '$lib/constants';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAllSharedLinks } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import type { LayoutLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
if (!UUID_REGEX.test(params.id)) {
redirect(302, AppRoute.SHARED_LINKS);
}
const [sharedLink] = await getAllSharedLinks({ id: params.id });
if (!sharedLink) {
redirect(302, AppRoute.SHARED_LINKS);
}
const $t = await getFormatter();
return {
sharedLink,
meta: {
title: $t('shared_links'),
},
};
}) satisfies LayoutLoad;
@@ -0,0 +1,96 @@
<script lang="ts">
import { goto } from '$app/navigation';
import SharedLinkExpiration from '$lib/components/SharedLinkExpiration.svelte';
import { AppRoute } from '$lib/constants';
import { handleUpdateSharedLink } from '$lib/services/shared-link.service';
import { SharedLinkType } from '@immich/sdk';
import { Field, FormModal, Input, PasswordInput, Switch, Text } from '@immich/ui';
import { mdiLink } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
data: PageData;
};
let { data }: Props = $props();
const sharedLink = $state(data.sharedLink);
let description = $state(sharedLink.description ?? '');
let allowDownload = $state(sharedLink.allowDownload);
let allowUpload = $state(sharedLink.allowUpload);
let showMetadata = $state(sharedLink.showMetadata);
let password = $state(sharedLink.password ?? '');
let slug = $state(sharedLink.slug ?? '');
let shareType = sharedLink.album ? SharedLinkType.Album : SharedLinkType.Individual;
let expiresAt = $state(sharedLink.expiresAt);
const onClose = async () => {
await goto(`${AppRoute.SHARED_LINKS}`);
};
const onSubmit = async () => {
const success = await handleUpdateSharedLink(sharedLink, {
description,
password: password ?? null,
expiresAt,
allowUpload,
allowDownload,
showMetadata,
slug: slug.trim() ?? null,
});
if (success) {
await onClose();
}
};
</script>
<FormModal title={$t('edit_link')} icon={mdiLink} {onClose} {onSubmit} submitText={$t('confirm')} size="small">
{#if shareType === SharedLinkType.Album}
<div class="text-sm">
{$t('public_album')} |
<span class="text-primary">{sharedLink.album?.albumName}</span>
</div>
{/if}
{#if shareType === SharedLinkType.Individual}
<div class="text-sm">
{$t('individual_share')} |
<span class="text-primary">{sharedLink.description || ''}</span>
</div>
{/if}
<div class="flex flex-col gap-4 mt-4">
<div>
<Field label={$t('custom_url')} description={$t('shared_link_custom_url_description')}>
<Input bind:value={slug} autocomplete="off" />
</Field>
{#if slug}
<Text size="tiny" color="muted" class="pt-2">/s/{encodeURIComponent(slug)}</Text>
{/if}
</div>
<Field label={$t('password')} description={$t('shared_link_password_description')}>
<PasswordInput bind:value={password} autocomplete="new-password" />
</Field>
<Field label={$t('description')}>
<Input bind:value={description} autocomplete="off" />
</Field>
<SharedLinkExpiration createdAt={sharedLink.createdAt} bind:expiresAt />
<Field label={$t('show_metadata')}>
<Switch bind:checked={showMetadata} />
</Field>
<Field label={$t('allow_public_user_to_download')} disabled={!showMetadata}>
<Switch bind:checked={allowDownload} />
</Field>
<Field label={$t('allow_public_user_to_upload')}>
<Switch bind:checked={allowUpload} />
</Field>
</div>
</FormModal>
@@ -0,0 +1,15 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url);
const $t = await getFormatter();
return {
meta: {
title: $t('shared_links'),
},
};
}) satisfies PageLoad;