mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 15:38:24 -08:00
feat: shared link edit (#24783)
This commit is contained in:
13
mobile/openapi/lib/api/shared_links_api.dart
generated
13
mobile/openapi/lib/api/shared_links_api.dart
generated
@@ -160,7 +160,9 @@ class SharedLinksApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId:
|
||||
Future<Response> getAllSharedLinksWithHttpInfo({ String? albumId, }) async {
|
||||
///
|
||||
/// * [String] id:
|
||||
Future<Response> 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 = <String>[];
|
||||
|
||||
@@ -196,8 +201,10 @@ class SharedLinksApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] albumId:
|
||||
Future<List<SharedLinkResponseDto>?> getAllSharedLinks({ String? albumId, }) async {
|
||||
final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, );
|
||||
///
|
||||
/// * [String] id:
|
||||
Future<List<SharedLinkResponseDto>?> 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));
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Album | null>().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();
|
||||
|
||||
@@ -19,9 +19,9 @@ import { getExternalDomain, OpenGraphTags } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class SharedLinkService extends BaseService {
|
||||
async getAll(auth: AuthDto, { albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
|
||||
async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
|
||||
return this.sharedLinkRepository
|
||||
.getAll({ userId: auth.user.id, albumId })
|
||||
.getAll({ userId: auth.user.id, id, albumId })
|
||||
.then((links) => links.map((link) => mapSharedLink(link)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
14
web/src/routes/(user)/shared-links/(list)/+layout.ts
Normal file
14
web/src/routes/(user)/shared-links/(list)/+layout.ts
Normal file
@@ -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;
|
||||
28
web/src/routes/(user)/shared-links/(list)/[id]/+layout.ts
Normal file
28
web/src/routes/(user)/shared-links/(list)/[id]/+layout.ts
Normal file
@@ -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>
|
||||
15
web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.ts
Normal file
15
web/src/routes/(user)/shared-links/(list)/[id]/edit/+page.ts
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user