From dea95ac2e6ee3e41441886ca59376200978c1b55 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 10 Nov 2025 15:49:02 -0500 Subject: [PATCH] refactor: shared-link service (#23770) --- pnpm-lock.yaml | 18 ++--- web/package.json | 2 +- web/src/lib/components/ActionButton.svelte | 19 +++++ .../album-page/album-shared-link.svelte | 20 ++--- .../search-bar/search-bar.svelte | 2 +- .../actions/shared-link-copy.svelte | 28 ------- .../actions/shared-link-delete.svelte | 26 ------ .../actions/shared-link-edit.svelte | 33 -------- .../sharedlinks-page/shared-link-card.svelte | 30 +++---- web/src/lib/managers/event-manager.svelte.ts | 1 + web/src/lib/services/shared-link.service.ts | 80 +++++++++++++++++-- .../shared-links/[[id=id]]/+page.svelte | 32 ++------ 12 files changed, 128 insertions(+), 163 deletions(-) create mode 100644 web/src/lib/components/ActionButton.svelte delete mode 100644 web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte delete mode 100644 web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte delete mode 100644 web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da1dd18d97..00087e91e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -684,8 +684,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.40.2 - version: 0.40.2(@internationalized/date@3.8.2)(svelte@5.43.0) + specifier: ^0.43.0 + version: 0.43.0(@internationalized/date@3.8.2)(svelte@5.43.0) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -2776,13 +2776,13 @@ packages: '@immich/justified-layout-wasm@0.4.3': resolution: {integrity: sha512-fpcQ7zPhP3Cp1bEXhONVYSUeIANa2uzaQFGKufUZQo5FO7aFT77szTVChhlCy4XaVy5R4ZvgSkA/1TJmeORz7Q==} - '@immich/svelte-markdown-preprocess@0.0.1': - resolution: {integrity: sha512-1vWoT4LO6fEyxrKwLKiNFECEkRVbuvpYPDvA7LavObTt2ijnonPYBDgfTwCPTofjxcocIGYUayv3CzgOzFiMOA==} + '@immich/svelte-markdown-preprocess@0.1.0': + resolution: {integrity: sha512-jgSOJEGLPKEXQCNRI4r4YUayeM2b0ZYLdzgKGl891jZBhOQIetlY7rU44kPpV1AA3/8wGDwNFKduIQZZ/qJYzg==} peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.40.2': - resolution: {integrity: sha512-6NS4yVx0VoyH+AaM7TISDaoIzZe3RuDOi6xMkK2LrOPQbKwTuheD2iagxsRYzUtJ9IPrmCPrwRBc9Jq5BkvmBQ==} + '@immich/ui@0.43.0': + resolution: {integrity: sha512-dwWIURsGghsbeFnqxCqUyWslyRU2vQjih7uewNr0nsW68bJ5/esl+V/Kiw2opiNiwI4Q3HEcuTRY57k4Hq+X3Q==} peerDependencies: svelte: ^5.0.0 @@ -14346,13 +14346,13 @@ snapshots: '@immich/justified-layout-wasm@0.4.3': {} - '@immich/svelte-markdown-preprocess@0.0.1(svelte@5.43.0)': + '@immich/svelte-markdown-preprocess@0.1.0(svelte@5.43.0)': dependencies: svelte: 5.43.0 - '@immich/ui@0.40.2(@internationalized/date@3.8.2)(svelte@5.43.0)': + '@immich/ui@0.43.0(@internationalized/date@3.8.2)(svelte@5.43.0)': dependencies: - '@immich/svelte-markdown-preprocess': 0.0.1(svelte@5.43.0) + '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.0) '@mdi/js': 7.4.47 bits-ui: 2.9.8(@internationalized/date@3.8.2)(svelte@5.43.0) luxon: 3.7.2 diff --git a/web/package.json b/web/package.json index b9ec394b5d..2a93230c24 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.40.2", + "@immich/ui": "^0.43.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.11.5", diff --git a/web/src/lib/components/ActionButton.svelte b/web/src/lib/components/ActionButton.svelte new file mode 100644 index 0000000000..1bbbf642e0 --- /dev/null +++ b/web/src/lib/components/ActionButton.svelte @@ -0,0 +1,19 @@ + + + onSelect?.({ event, item: action })} +/> diff --git a/web/src/lib/components/album-page/album-shared-link.svelte b/web/src/lib/components/album-page/album-shared-link.svelte index 580a865ae6..1b6db6ff69 100644 --- a/web/src/lib/components/album-page/album-shared-link.svelte +++ b/web/src/lib/components/album-page/album-shared-link.svelte @@ -1,10 +1,9 @@
@@ -40,14 +41,7 @@ {getShareProperties()}
- handleShowSharedLinkQrCode(sharedLink)} - /> - + +
diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 8c1d2c5d08..ea1147fe06 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -92,7 +92,7 @@ } const result = modalManager.open(SearchFilterModal, { searchQuery }); - close = () => result.close(undefined); + close = () => result.close(); closeDropdown(); const searchResult = await result.onClose; diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte deleted file mode 100644 index 06369e7792..0000000000 --- a/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - -{#if menuItem} - handleCopySharedLinkUrl(sharedLink)} /> -{:else} - handleCopySharedLinkUrl(sharedLink)} - /> -{/if} diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte deleted file mode 100644 index f844d8483e..0000000000 --- a/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -{#if menuItem} - -{:else} - -{/if} diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte deleted file mode 100644 index 7482c1b6eb..0000000000 --- a/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - -{#if menuItem} - -{:else} - -{/if} diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte index f811f60e77..b2c6cf296d 100644 --- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte +++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte @@ -1,23 +1,19 @@
- - - - - +
diff --git a/web/src/lib/managers/event-manager.svelte.ts b/web/src/lib/managers/event-manager.svelte.ts index 44ee524658..e7d50f026d 100644 --- a/web/src/lib/managers/event-manager.svelte.ts +++ b/web/src/lib/managers/event-manager.svelte.ts @@ -10,6 +10,7 @@ export type Events = { ThemeChange: [ThemeSetting]; SharedLinkCreate: [SharedLinkResponseDto]; SharedLinkUpdate: [SharedLinkResponseDto]; + SharedLinkDelete: [SharedLinkResponseDto]; }; type Listener, K extends keyof EventMap> = (...params: EventMap[K]) => void; diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts index 529b6b23b4..a48f0b5965 100644 --- a/web/src/lib/services/shared-link.service.ts +++ b/web/src/lib/services/shared-link.service.ts @@ -1,3 +1,5 @@ +import { goto } from '$app/navigation'; +import { AppRoute } from '$lib/constants'; import { eventManager } from '$lib/managers/event-manager.svelte'; import QrCodeModal from '$lib/modals/QrCodeModal.svelte'; import { serverConfig } from '$lib/stores/server-config.store'; @@ -6,15 +8,58 @@ import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; import { createSharedLink, + removeSharedLink, updateSharedLink, type SharedLinkCreateDto, type SharedLinkEditDto, type SharedLinkResponseDto, } from '@immich/sdk'; -import { modalManager, toastManager } from '@immich/ui'; +import { MenuItemType, menuManager, modalManager, toastManager, type MenuItem } from '@immich/ui'; +import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiDotsVertical, mdiQrcode } from '@mdi/js'; +import type { MessageFormatter } from 'svelte-i18n'; import { get } from 'svelte/store'; -const makeSharedLinkUrl = (sharedLink: SharedLinkResponseDto) => { +export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLinkResponseDto) => { + const Edit: MenuItem = { + title: $t('edit_link'), + icon: mdiCircleEditOutline, + onSelect: () => void goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`), + }; + + const Delete: MenuItem = { + title: $t('delete_link'), + icon: mdiDelete, + color: 'danger', + onSelect: () => void handleDeleteSharedLink(sharedLink), + }; + + const Copy: MenuItem = { + title: $t('copy_link'), + icon: mdiContentCopy, + onSelect: () => void copyToClipboard(asUrl(sharedLink)), + }; + + const ViewQrCode: MenuItem = { + title: $t('view_qr_code'), + icon: mdiQrcode, + onSelect: () => void handleShowSharedLinkQrCode(sharedLink), + }; + + const ContextMenu: MenuItem = { + title: $t('shared_link_options'), + icon: mdiDotsVertical, + onSelect: ({ event }) => + void menuManager.show({ + target: event.currentTarget as HTMLElement, + position: 'top-right', + items: [Edit, Copy, MenuItemType.Divider, Delete], + }), + }; + + return { Edit, Delete, Copy, ViewQrCode, ContextMenu }; +}; + +const asUrl = (sharedLink: SharedLinkResponseDto) => { const path = sharedLink.slug ? `s/${sharedLink.slug}` : `share/${sharedLink.key}`; return new URL(path, get(serverConfig).externalDomain || globalThis.location.origin).href; }; @@ -54,11 +99,34 @@ export const handleUpdateSharedLink = async (sharedLink: SharedLinkResponseDto, } }; -export const handleShowSharedLinkQrCode = async (sharedLink: SharedLinkResponseDto) => { +export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto): Promise => { const $t = await getFormatter(); - await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) }); + + const success = await modalManager.showDialog({ + title: $t('delete_shared_link'), + prompt: $t('confirm_delete_shared_link'), + confirmText: $t('delete'), + }); + + if (!success) { + return false; + } + + try { + await removeSharedLink({ id: sharedLink.id }); + + eventManager.emit('SharedLinkDelete', sharedLink); + + toastManager.success($t('deleted_shared_link')); + + return true; + } catch (error) { + handleError(error, $t('errors.unable_to_delete_shared_link')); + return false; + } }; -export const handleCopySharedLinkUrl = async (sharedLink: SharedLinkResponseDto) => { - await copyToClipboard(makeSharedLinkUrl(sharedLink)); +const handleShowSharedLinkQrCode = async (sharedLink: SharedLinkResponseDto) => { + const $t = await getFormatter(); + await modalManager.show(QrCodeModal, { title: $t('view_link'), value: asUrl(sharedLink) }); }; diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte index 4b63ce71a1..e0239a2a43 100644 --- a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte +++ b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte @@ -7,9 +7,7 @@ import { AppRoute } from '$lib/constants'; import GroupTab from '$lib/elements/GroupTab.svelte'; import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; - import { handleError } from '$lib/utils/handle-error'; - import { getAllSharedLinks, removeSharedLink, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk'; - import { modalManager, toastManager } from '@immich/ui'; + import { getAllSharedLinks, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -31,26 +29,6 @@ await refresh(); }); - const handleDeleteLink = async (id: string) => { - const isConfirmed = await modalManager.showDialog({ - title: $t('delete_shared_link'), - prompt: $t('confirm_delete_shared_link'), - confirmText: $t('delete'), - }); - - if (!isConfirmed) { - return; - } - - try { - await removeSharedLink({ id }); - toastManager.success($t('deleted_shared_link')); - await refresh(); - } catch (error) { - handleError(error, $t('errors.unable_to_delete_shared_link')); - } - }; - type Filter = 'all' | 'album' | 'individual'; const filterMap: Record = { @@ -87,9 +65,13 @@ sharedLinks[index] = sharedLink; } }; + + const onSharedLinkDelete = (sharedLink: SharedLinkResponseDto) => { + sharedLinks = sharedLinks.filter(({ id }) => id !== sharedLink.id); + }; - + {#snippet buttons()} @@ -108,7 +90,7 @@ {:else}
{#each filteredSharedLinks as sharedLink (sharedLink.id)} - handleDeleteLink(sharedLink.id)} /> + {/each}
{/if}