diff --git a/mobile/openapi/lib/api/shared_links_api.dart b/mobile/openapi/lib/api/shared_links_api.dart index 79106e5db6..587a9640b4 100644 --- a/mobile/openapi/lib/api/shared_links_api.dart +++ b/mobile/openapi/lib/api/shared_links_api.dart @@ -160,7 +160,9 @@ class SharedLinksApi { /// Parameters: /// /// * [String] albumId: - Future getAllSharedLinksWithHttpInfo({ String? albumId, }) async { + /// + /// * [String] id: + Future getAllSharedLinksWithHttpInfo({ String? albumId, String? id, }) async { // ignore: prefer_const_declarations final apiPath = r'/shared-links'; @@ -174,6 +176,9 @@ class SharedLinksApi { if (albumId != null) { queryParams.addAll(_queryParams('', 'albumId', albumId)); } + if (id != null) { + queryParams.addAll(_queryParams('', 'id', id)); + } const contentTypes = []; @@ -196,8 +201,10 @@ class SharedLinksApi { /// Parameters: /// /// * [String] albumId: - Future?> getAllSharedLinks({ String? albumId, }) async { - final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, ); + /// + /// * [String] id: + Future?> getAllSharedLinks({ String? albumId, String? id, }) async { + final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, id: id, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4a51e3b88c..7e859ffee0 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -10381,6 +10381,21 @@ "format": "uuid", "type": "string" } + }, + { + "name": "id", + "required": false, + "in": "query", + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + } + ], + "schema": { + "format": "uuid", + "type": "string" + } } ], "responses": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2f6ce9208d..b7c980f4f4 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -4218,14 +4218,16 @@ export function lockSession({ id }: { /** * Retrieve all shared links */ -export function getAllSharedLinks({ albumId }: { +export function getAllSharedLinks({ albumId, id }: { albumId?: string; + id?: string; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: SharedLinkResponseDto[]; }>(`/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 011707f1f7..b2aad8957e 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 7bfa9ac6ae..8fab087156 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 3c1a6083e9..199f0bf7a7 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 f3bdd42a89..0000000000 --- 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 cbea6ddd9d..9f70024193 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 cc9afd4f64..ce14870923 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 0000000000..842940ffe1 --- /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 0000000000..e69de29bb2 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 0000000000..c3f62e36c3 --- /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 0000000000..6fef129583 --- /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 0000000000..afbe9991ff --- /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;