From 1c156a179b1733639058fcd5d3e7c15eeda4bd3a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 22 Dec 2025 11:47:06 -0500 Subject: [PATCH] feat: shared link edit (#24783) --- mobile/openapi/lib/api/shared_links_api.dart | Bin 16730 -> 16898 bytes open-api/immich-openapi-specs.json | 15 +++ open-api/typescript-sdk/src/fetch-client.ts | 6 +- server/src/dtos/shared-link.dto.ts | 5 + .../repositories/shared-link.repository.ts | 4 +- server/src/services/shared-link.service.ts | 4 +- .../lib/modals/SharedLinkUpdateModal.svelte | 98 ------------------ web/src/lib/services/shared-link.service.ts | 2 +- .../+page.svelte => (list)/+layout.svelte} | 20 ++-- .../(user)/shared-links/(list)/+layout.ts | 14 +++ .../(user)/shared-links/(list)/+page.svelte | 0 .../{[[id=id]] => (list)}/+page.ts | 0 .../shared-links/(list)/[id]/+layout.ts | 28 +++++ .../(list)/[id]/edit/+page.svelte | 96 +++++++++++++++++ .../shared-links/(list)/[id]/edit/+page.ts | 15 +++ 15 files changed, 192 insertions(+), 115 deletions(-) delete mode 100644 web/src/lib/modals/SharedLinkUpdateModal.svelte rename web/src/routes/(user)/shared-links/{[[id=id]]/+page.svelte => (list)/+layout.svelte} (85%) create mode 100644 web/src/routes/(user)/shared-links/(list)/+layout.ts create mode 100644 web/src/routes/(user)/shared-links/(list)/+page.svelte rename web/src/routes/(user)/shared-links/{[[id=id]] => (list)}/+page.ts (100%) create mode 100644 web/src/routes/(user)/shared-links/(list)/[id]/+layout.ts create mode 100644 web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.svelte create mode 100644 web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.ts diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 79106e5db663281c16ae20a32b751fc3cac47b3c..587a9640b440d6425928220a98eb25aba44e740d 100644 GIT binary patch delta 119 zcmccB#MsorxM8XAbsP7YAJH#uIc8m#r2m>(xx>tsdoD7MTLE1<&7`HBx1C(pI0n*2;mY_hhc K)MjR>aCHEF$|yGg delta 55 zcmV-70LcG>gaO)u0kEYLlVv3ola?0_llvD9lkXD~ld>FFlRXVPlb{skv)2`T0h4YQ NN|VGFSF(`/shared-links${QS.query(QS.explode({ - albumId + albumId, + id }))}`, { ...opts })); diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 011707f1f..b2aad8957 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; import _ from 'lodash'; import { SharedLink } from 'src/database'; +import { HistoryBuilder, Property } from 'src/decorators'; import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkType } from 'src/enum'; @@ -10,6 +11,10 @@ import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } f export class SharedLinkSearchDto { @ValidateUUID({ optional: true }) albumId?: string; + + @ValidateUUID({ optional: true }) + @Property({ history: new HistoryBuilder().added('v2.5.0') }) + id?: string; } export class SharedLinkCreateDto { diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts index 7bfa9ac6a..8fab08715 100644 --- a/server/src/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -12,6 +12,7 @@ import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; export type SharedLinkSearchOptions = { userId: string; + id?: string; albumId?: string; }; @@ -118,7 +119,7 @@ export class SharedLinkRepository { } @GenerateSql({ params: [{ userId: DummyValue.UUID, albumId: DummyValue.UUID }] }) - getAll({ userId, albumId }: SharedLinkSearchOptions) { + getAll({ userId, id, albumId }: SharedLinkSearchOptions) { return this.db .selectFrom('shared_link') .selectAll('shared_link') @@ -176,6 +177,7 @@ export class SharedLinkRepository { .select((eb) => eb.fn.toJson('album').$castTo().as('album')) .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)])) .$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!)) + .$if(!!id, (eb) => eb.where('shared_link.id', '=', id!)) .orderBy('shared_link.createdAt', 'desc') .distinctOn(['shared_link.createdAt']) .execute(); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 3c1a6083e..199f0bf7a 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -19,9 +19,9 @@ import { getExternalDomain, OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService extends BaseService { - async getAll(auth: AuthDto, { albumId }: SharedLinkSearchDto): Promise { + async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise { return this.sharedLinkRepository - .getAll({ userId: auth.user.id, albumId }) + .getAll({ userId: auth.user.id, id, albumId }) .then((links) => links.map((link) => mapSharedLink(link))); } diff --git a/web/src/lib/modals/SharedLinkUpdateModal.svelte b/web/src/lib/modals/SharedLinkUpdateModal.svelte deleted file mode 100644 index f3bdd42a8..000000000 --- a/web/src/lib/modals/SharedLinkUpdateModal.svelte +++ /dev/null @@ -1,98 +0,0 @@ - - - - - {#if shareType === SharedLinkType.Album} -
- {$t('public_album')} | - {sharedLink.album?.albumName} -
- {/if} - - {#if shareType === SharedLinkType.Individual} -
- {$t('individual_share')} | - {sharedLink.description || ''} -
- {/if} - -
-
- - - - {#if slug} - /s/{encodeURIComponent(slug)} - {/if} -
- - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - -
diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts index cbea6ddd9..9f7002419 100644 --- a/web/src/lib/services/shared-link.service.ts +++ b/web/src/lib/services/shared-link.service.ts @@ -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 = { diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte b/web/src/routes/(user)/shared-links/(list)/+layout.svelte similarity index 85% rename from web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte rename to web/src/routes/(user)/shared-links/(list)/+layout.svelte index cc9afd4f6..ce1487092 100644 --- a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte +++ b/web/src/routes/(user)/shared-links/(list)/+layout.svelte @@ -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 @@ {/snippet} -
+ {#if sharedLinks.length === 0}
{/if} - {#if sharedLink} - goto(AppRoute.SHARED_LINKS)} /> - {/if} -
+ {@render children?.()} +
diff --git a/web/src/routes/(user)/shared-links/(list)/+layout.ts b/web/src/routes/(user)/shared-links/(list)/+layout.ts new file mode 100644 index 000000000..842940ffe --- /dev/null +++ b/web/src/routes/(user)/shared-links/(list)/+layout.ts @@ -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; diff --git a/web/src/routes/(user)/shared-links/(list)/+page.svelte b/web/src/routes/(user)/shared-links/(list)/+page.svelte new file mode 100644 index 000000000..e69de29bb diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.ts b/web/src/routes/(user)/shared-links/(list)/+page.ts similarity index 100% rename from web/src/routes/(user)/shared-links/[[id=id]]/+page.ts rename to web/src/routes/(user)/shared-links/(list)/+page.ts diff --git a/web/src/routes/(user)/shared-links/(list)/[id]/+layout.ts b/web/src/routes/(user)/shared-links/(list)/[id]/+layout.ts new file mode 100644 index 000000000..c3f62e36c --- /dev/null +++ b/web/src/routes/(user)/shared-links/(list)/[id]/+layout.ts @@ -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; diff --git a/web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.svelte b/web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.svelte new file mode 100644 index 000000000..6fef12958 --- /dev/null +++ b/web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.svelte @@ -0,0 +1,96 @@ + + + + {#if shareType === SharedLinkType.Album} +
+ {$t('public_album')} | + {sharedLink.album?.albumName} +
+ {/if} + + {#if shareType === SharedLinkType.Individual} +
+ {$t('individual_share')} | + {sharedLink.description || ''} +
+ {/if} + +
+
+ + + + {#if slug} + /s/{encodeURIComponent(slug)} + {/if} +
+ + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.ts b/web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.ts new file mode 100644 index 000000000..afbe9991f --- /dev/null +++ b/web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.ts @@ -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;