1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

feat(server/web): album description (#3558)

* feat(server): add album description

* chore: open api

* fix: tests

* show and edit description on the web

* fix test

* remove unused code

* type event

* format fix

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2023-08-05 22:43:26 -04:00 committed by GitHub
parent deaf81e2a4
commit 2f26a7edae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 222 additions and 36 deletions

View file

@ -210,6 +210,12 @@ export interface AlbumResponseDto {
* @memberof AlbumResponseDto * @memberof AlbumResponseDto
*/ */
'createdAt': string; 'createdAt': string;
/**
*
* @type {string}
* @memberof AlbumResponseDto
*/
'description': string;
/** /**
* *
* @type {string} * @type {string}
@ -865,6 +871,12 @@ export interface CreateAlbumDto {
* @memberof CreateAlbumDto * @memberof CreateAlbumDto
*/ */
'assetIds'?: Array<string>; 'assetIds'?: Array<string>;
/**
*
* @type {string}
* @memberof CreateAlbumDto
*/
'description'?: string;
/** /**
* *
* @type {Array<string>} * @type {Array<string>}
@ -2843,6 +2855,12 @@ export interface UpdateAlbumDto {
* @memberof UpdateAlbumDto * @memberof UpdateAlbumDto
*/ */
'albumThumbnailAssetId'?: string; 'albumThumbnailAssetId'?: string;
/**
*
* @type {string}
* @memberof UpdateAlbumDto
*/
'description'?: string;
} }
/** /**
* *

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -4754,6 +4754,9 @@
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"
}, },
"description": {
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@ -4786,6 +4789,7 @@
"id", "id",
"ownerId", "ownerId",
"albumName", "albumName",
"description",
"createdAt", "createdAt",
"updatedAt", "updatedAt",
"albumThumbnailAssetId", "albumThumbnailAssetId",
@ -5264,6 +5268,9 @@
}, },
"type": "array" "type": "array"
}, },
"description": {
"type": "string"
},
"sharedWithUserIds": { "sharedWithUserIds": {
"items": { "items": {
"format": "uuid", "format": "uuid",
@ -6903,6 +6910,9 @@
"albumThumbnailAssetId": { "albumThumbnailAssetId": {
"format": "uuid", "format": "uuid",
"type": "string" "type": "string"
},
"description": {
"type": "string"
} }
}, },
"type": "object" "type": "object"

View file

@ -7,6 +7,7 @@ export class AlbumResponseDto {
id!: string; id!: string;
ownerId!: string; ownerId!: string;
albumName!: string; albumName!: string;
description!: string;
createdAt!: Date; createdAt!: Date;
updatedAt!: Date; updatedAt!: Date;
albumThumbnailAssetId!: string | null; albumThumbnailAssetId!: string | null;
@ -19,7 +20,7 @@ export class AlbumResponseDto {
lastModifiedAssetTimestamp?: Date; lastModifiedAssetTimestamp?: Date;
} }
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { const _map = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
const sharedUsers: UserResponseDto[] = []; const sharedUsers: UserResponseDto[] = [];
entity.sharedUsers?.forEach((user) => { entity.sharedUsers?.forEach((user) => {
@ -29,6 +30,7 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
return { return {
albumName: entity.albumName, albumName: entity.albumName,
description: entity.description,
albumThumbnailAssetId: entity.albumThumbnailAssetId, albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt, createdAt: entity.createdAt,
updatedAt: entity.updatedAt, updatedAt: entity.updatedAt,
@ -37,33 +39,13 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
owner: mapUser(entity.owner), owner: mapUser(entity.owner),
sharedUsers, sharedUsers,
shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0, shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0,
assets: entity.assets?.map((asset) => mapAsset(asset)) || [], assets: withAssets ? entity.assets?.map((asset) => mapAsset(asset)) || [] : [],
assetCount: entity.assets?.length || 0, assetCount: entity.assets?.length || 0,
}; };
} };
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto { export const mapAlbum = (entity: AlbumEntity) => _map(entity, true);
const sharedUsers: UserResponseDto[] = []; export const mapAlbumExcludeAssetInfo = (entity: AlbumEntity) => _map(entity, false);
entity.sharedUsers?.forEach((user) => {
const userDto = mapUser(user);
sharedUsers.push(userDto);
});
return {
albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
id: entity.id,
ownerId: entity.ownerId,
owner: mapUser(entity.owner),
sharedUsers,
shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0,
assets: [],
assetCount: entity.assets?.length || 0,
};
}
export class AlbumCountResponseDto { export class AlbumCountResponseDto {
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })

View file

@ -156,6 +156,7 @@ describe(AlbumService.name, () => {
await expect(sut.create(authStub.admin, { albumName: 'Empty album' })).resolves.toEqual({ await expect(sut.create(authStub.admin, { albumName: 'Empty album' })).resolves.toEqual({
albumName: 'Empty album', albumName: 'Empty album',
description: '',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
assetCount: 0, assetCount: 0,
assets: [], assets: [],

View file

@ -94,6 +94,7 @@ export class AlbumService {
const album = await this.albumRepository.create({ const album = await this.albumRepository.create({
ownerId: authUser.id, ownerId: authUser.id,
albumName: dto.albumName, albumName: dto.albumName,
description: dto.description,
sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [], sharedUsers: dto.sharedWithUserIds?.map((value) => ({ id: value } as UserEntity)) ?? [],
assets: (dto.assetIds || []).map((id) => ({ id } as AssetEntity)), assets: (dto.assetIds || []).map((id) => ({ id } as AssetEntity)),
albumThumbnailAssetId: dto.assetIds?.[0] || null, albumThumbnailAssetId: dto.assetIds?.[0] || null,
@ -118,6 +119,7 @@ export class AlbumService {
const updatedAlbum = await this.albumRepository.update({ const updatedAlbum = await this.albumRepository.update({
id: album.id, id: album.id,
albumName: dto.albumName, albumName: dto.albumName,
description: dto.description,
albumThumbnailAssetId: dto.albumThumbnailAssetId, albumThumbnailAssetId: dto.albumThumbnailAssetId,
}); });

View file

@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { ValidateUUID } from '../../domain.util'; import { ValidateUUID } from '../../domain.util';
export class CreateAlbumDto { export class CreateAlbumDto {
@ -8,6 +8,10 @@ export class CreateAlbumDto {
@ApiProperty() @ApiProperty()
albumName!: string; albumName!: string;
@IsString()
@IsOptional()
description?: string;
@ValidateUUID({ optional: true, each: true }) @ValidateUUID({ optional: true, each: true })
sharedWithUserIds?: string[]; sharedWithUserIds?: string[];

View file

@ -1,12 +1,15 @@
import { ApiProperty } from '@nestjs/swagger'; import { IsOptional, IsString } from 'class-validator';
import { IsOptional } from 'class-validator';
import { ValidateUUID } from '../../domain.util'; import { ValidateUUID } from '../../domain.util';
export class UpdateAlbumDto { export class UpdateAlbumDto {
@IsOptional() @IsOptional()
@ApiProperty() @IsString()
albumName?: string; albumName?: string;
@IsOptional()
@IsString()
description?: string;
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
albumThumbnailAssetId?: string; albumThumbnailAssetId?: string;
} }

View file

@ -5,8 +5,8 @@ import {
AuthUserDto, AuthUserDto,
BulkIdResponseDto, BulkIdResponseDto,
BulkIdsDto, BulkIdsDto,
CreateAlbumDto, CreateAlbumDto as CreateDto,
UpdateAlbumDto, UpdateAlbumDto as UpdateDto,
} from '@app/domain'; } from '@app/domain';
import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto'; import { GetAlbumsDto } from '@app/domain/album/dto/get-albums.dto';
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
@ -34,7 +34,7 @@ export class AlbumController {
} }
@Post() @Post()
createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateAlbumDto) { createAlbum(@AuthUser() authUser: AuthUserDto, @Body() dto: CreateDto) {
return this.service.create(authUser, dto); return this.service.create(authUser, dto);
} }
@ -45,7 +45,7 @@ export class AlbumController {
} }
@Patch(':id') @Patch(':id')
updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateAlbumDto) { updateAlbumInfo(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateDto) {
return this.service.update(authUser, id, dto); return this.service.update(authUser, id, dto);
} }

View file

@ -27,6 +27,9 @@ export class AlbumEntity {
@Column({ default: 'Untitled Album' }) @Column({ default: 'Untitled Album' })
albumName!: string; albumName!: string;
@Column({ type: 'text', default: '' })
description!: string;
@CreateDateColumn({ type: 'timestamptz' }) @CreateDateColumn({ type: 'timestamptz' })
createdAt!: Date; createdAt!: Date;

View file

@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddAlbumDescription1691209138541 implements MigrationInterface {
name = 'AddAlbumDescription1691209138541';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "albums" ADD "description" text NOT NULL DEFAULT ''`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "description"`);
}
}

View file

@ -234,7 +234,7 @@ export class TypesenseRepository implements ISearchRepository {
.documents() .documents()
.search({ .search({
q: query, q: query,
query_by: 'albumName', query_by: ['albumName', 'description'].join(','),
filter_by: this.getAlbumFilters(filters), filter_by: this.getAlbumFilters(filters),
}); });

View file

@ -1,11 +1,12 @@
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections'; import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
export const albumSchemaVersion = 1; export const albumSchemaVersion = 2;
export const albumSchema: CollectionCreateSchema = { export const albumSchema: CollectionCreateSchema = {
name: `albums-v${albumSchemaVersion}`, name: `albums-v${albumSchemaVersion}`,
fields: [ fields: [
{ name: 'ownerId', type: 'string', facet: false }, { name: 'ownerId', type: 'string', facet: false },
{ name: 'albumName', type: 'string', facet: false, sort: true }, { name: 'albumName', type: 'string', facet: false, sort: true },
{ name: 'description', type: 'string', facet: false },
{ name: 'createdAt', type: 'string', facet: false, sort: true }, { name: 'createdAt', type: 'string', facet: false, sort: true },
{ name: 'updatedAt', type: 'string', facet: false, sort: true }, { name: 'updatedAt', type: 'string', facet: false, sort: true },
], ],

View file

@ -4,7 +4,7 @@ import { SharedLinkType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import request from 'supertest'; import request from 'supertest';
import { errorStub } from '../fixtures'; import { errorStub, uuidStub } from '../fixtures';
import { api, db } from '../test-utils'; import { api, db } from '../test-utils';
const user1SharedUser = 'user1SharedUser'; const user1SharedUser = 'user1SharedUser';
@ -193,6 +193,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
updatedAt: expect.any(String), updatedAt: expect.any(String),
ownerId: user1.userId, ownerId: user1.userId,
albumName: 'New album', albumName: 'New album',
description: '',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
shared: false, shared: false,
sharedUsers: [], sharedUsers: [],
@ -202,4 +203,32 @@ describe(`${AlbumController.name} (e2e)`, () => {
}); });
}); });
}); });
describe('PATCH /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
.patch(`/album/${uuidStub.notFound}`)
.send({ albumName: 'New album name' });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should update an album', async () => {
const album = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' });
const { status, body } = await request(server)
.patch(`/album/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
albumName: 'New album name',
description: 'An album description',
});
expect(status).toBe(200);
expect(body).toEqual({
...album,
updatedAt: expect.any(String),
albumName: 'New album name',
description: 'An album description',
});
});
});
}); });

View file

@ -7,6 +7,7 @@ export const albumStub = {
empty: Object.freeze<AlbumEntity>({ empty: Object.freeze<AlbumEntity>({
id: 'album-1', id: 'album-1',
albumName: 'Empty album', albumName: 'Empty album',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
@ -20,6 +21,7 @@ export const albumStub = {
sharedWithUser: Object.freeze<AlbumEntity>({ sharedWithUser: Object.freeze<AlbumEntity>({
id: 'album-2', id: 'album-2',
albumName: 'Empty album shared with user', albumName: 'Empty album shared with user',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
@ -33,6 +35,7 @@ export const albumStub = {
sharedWithMultiple: Object.freeze<AlbumEntity>({ sharedWithMultiple: Object.freeze<AlbumEntity>({
id: 'album-3', id: 'album-3',
albumName: 'Empty album shared with users', albumName: 'Empty album shared with users',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
@ -46,6 +49,7 @@ export const albumStub = {
sharedWithAdmin: Object.freeze<AlbumEntity>({ sharedWithAdmin: Object.freeze<AlbumEntity>({
id: 'album-3', id: 'album-3',
albumName: 'Empty album shared with admin', albumName: 'Empty album shared with admin',
description: '',
ownerId: authStub.user1.id, ownerId: authStub.user1.id,
owner: userStub.user1, owner: userStub.user1,
assets: [], assets: [],
@ -59,6 +63,7 @@ export const albumStub = {
oneAsset: Object.freeze<AlbumEntity>({ oneAsset: Object.freeze<AlbumEntity>({
id: 'album-4', id: 'album-4',
albumName: 'Album with one asset', albumName: 'Album with one asset',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [assetStub.image], assets: [assetStub.image],
@ -72,6 +77,7 @@ export const albumStub = {
twoAssets: Object.freeze<AlbumEntity>({ twoAssets: Object.freeze<AlbumEntity>({
id: 'album-4a', id: 'album-4a',
albumName: 'Album with two assets', albumName: 'Album with two assets',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [assetStub.image, assetStub.withLocation], assets: [assetStub.image, assetStub.withLocation],
@ -85,6 +91,7 @@ export const albumStub = {
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({ emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5', id: 'album-5',
albumName: 'Empty album with invalid thumbnail', albumName: 'Empty album with invalid thumbnail',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
@ -98,6 +105,7 @@ export const albumStub = {
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({ emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5', id: 'album-5',
albumName: 'Empty album with invalid thumbnail', albumName: 'Empty album with invalid thumbnail',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [], assets: [],
@ -111,6 +119,7 @@ export const albumStub = {
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({ oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6', id: 'album-6',
albumName: 'Album with one asset and invalid thumbnail', albumName: 'Album with one asset and invalid thumbnail',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [assetStub.image], assets: [assetStub.image],
@ -124,6 +133,7 @@ export const albumStub = {
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({ oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6', id: 'album-6',
albumName: 'Album with one asset and invalid thumbnail', albumName: 'Album with one asset and invalid thumbnail',
description: '',
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
assets: [assetStub.image], assets: [assetStub.image],

View file

@ -68,6 +68,7 @@ const assetResponse: AssetResponseDto = {
const albumResponse: AlbumResponseDto = { const albumResponse: AlbumResponseDto = {
albumName: 'Test Album', albumName: 'Test Album',
description: '',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
createdAt: today, createdAt: today,
updatedAt: today, updatedAt: today,
@ -146,6 +147,7 @@ export const sharedLinkStub = {
ownerId: authStub.admin.id, ownerId: authStub.admin.id,
owner: userStub.admin, owner: userStub.admin,
albumName: 'Test Album', albumName: 'Test Album',
description: '',
createdAt: today, createdAt: today,
updatedAt: today, updatedAt: today,
albumThumbnailAsset: null, albumThumbnailAsset: null,

View file

@ -210,6 +210,12 @@ export interface AlbumResponseDto {
* @memberof AlbumResponseDto * @memberof AlbumResponseDto
*/ */
'createdAt': string; 'createdAt': string;
/**
*
* @type {string}
* @memberof AlbumResponseDto
*/
'description': string;
/** /**
* *
* @type {string} * @type {string}
@ -865,6 +871,12 @@ export interface CreateAlbumDto {
* @memberof CreateAlbumDto * @memberof CreateAlbumDto
*/ */
'assetIds'?: Array<string>; 'assetIds'?: Array<string>;
/**
*
* @type {string}
* @memberof CreateAlbumDto
*/
'description'?: string;
/** /**
* *
* @type {Array<string>} * @type {Array<string>}
@ -2843,6 +2855,12 @@ export interface UpdateAlbumDto {
* @memberof UpdateAlbumDto * @memberof UpdateAlbumDto
*/ */
'albumThumbnailAssetId'?: string; 'albumThumbnailAssetId'?: string;
/**
*
* @type {string}
* @memberof UpdateAlbumDto
*/
'description'?: string;
} }
/** /**
* *

View file

@ -44,6 +44,7 @@
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import { downloadArchive } from '../../utils/asset-utils'; import { downloadArchive } from '../../utils/asset-utils';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import EditDescriptionModal from './edit-description-modal.svelte';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let sharedLink: SharedLinkResponseDto | undefined = undefined; export let sharedLink: SharedLinkResponseDto | undefined = undefined;
@ -73,6 +74,7 @@
let isShowAlbumOptions = false; let isShowAlbumOptions = false;
let isShowThumbnailSelection = false; let isShowThumbnailSelection = false;
let isShowDeleteConfirmation = false; let isShowDeleteConfirmation = false;
let isEditingDescription = false;
let backUrl = '/albums'; let backUrl = '/albums';
let currentAlbumName = ''; let currentAlbumName = '';
@ -298,6 +300,27 @@
const handleSelectAll = () => { const handleSelectAll = () => {
multiSelectAsset = new Set(album.assets); multiSelectAsset = new Set(album.assets);
}; };
const descriptionUpdatedHandler = (description: string) => {
try {
api.albumApi.updateAlbumInfo({
id: album.id,
updateAlbumDto: {
description,
},
});
album.description = description;
} catch (e) {
console.error('Error [descriptionUpdatedHandler] ', e);
notificationController.show({
type: NotificationType.Error,
message: 'Error setting album description, check console for more details',
});
}
isEditingDescription = false;
};
</script> </script>
<section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}> <section class="bg-immich-bg dark:bg-immich-dark-bg" class:hidden={isShowThumbnailSelection}>
@ -405,6 +428,7 @@
{/if} {/if}
<section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40"> <section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40">
<!-- ALBUM TITLE -->
<input <input
on:keydown={(e) => { on:keydown={(e) => {
if (e.key == 'Enter') { if (e.key == 'Enter') {
@ -421,8 +445,10 @@
bind:value={album.albumName} bind:value={album.albumName}
disabled={!isOwned} disabled={!isOwned}
bind:this={titleInput} bind:this={titleInput}
title="Edit Title"
/> />
<!-- ALBUM SUMMARY -->
{#if album.assetCount > 0} {#if album.assetCount > 0}
<span class="my-4 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details"> <span class="my-4 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
<p class="">{getDateRange()}</p> <p class="">{getDateRange()}</p>
@ -448,6 +474,17 @@
</div> </div>
{/if} {/if}
<!-- ALBUM DESCRIPTION -->
<button
class="mb-12 mt-6 w-full border-b-2 border-transparent pb-2 text-left text-lg font-medium transition-colors hover:border-b-2 dark:text-gray-300"
on:click={() => (isEditingDescription = true)}
class:hover:border-gray-400={isOwned}
disabled={!isOwned}
title="Edit description"
>
{album.description || 'Add description'}
</button>
{#if album.assetCount > 0 && !isShowAssetSelection} {#if album.assetCount > 0 && !isShowAssetSelection}
<GalleryViewer assets={album.assets} {sharedLink} bind:selectedAssets={multiSelectAsset} /> <GalleryViewer assets={album.assets} {sharedLink} bind:selectedAssets={multiSelectAsset} />
{:else} {:else}
@ -490,6 +527,7 @@
{#if isShowShareLinkModal} {#if isShowShareLinkModal}
<CreateSharedLinkModal on:close={() => (isShowShareLinkModal = false)} shareType={SharedLinkType.Album} {album} /> <CreateSharedLinkModal on:close={() => (isShowShareLinkModal = false)} shareType={SharedLinkType.Album} {album} />
{/if} {/if}
{#if isShowShareInfoModal} {#if isShowShareInfoModal}
<ShareInfoModal on:close={() => (isShowShareInfoModal = false)} {album} on:user-deleted={sharedUserDeletedHandler} /> <ShareInfoModal on:close={() => (isShowShareInfoModal = false)} {album} on:user-deleted={sharedUserDeletedHandler} />
{/if} {/if}
@ -515,3 +553,11 @@
</svelte:fragment> </svelte:fragment>
</ConfirmDialogue> </ConfirmDialogue>
{/if} {/if}
{#if isEditingDescription}
<EditDescriptionModal
{album}
on:close={() => (isEditingDescription = false)}
on:updated={({ detail: description }) => descriptionUpdatedHandler(description)}
/>
{/if}

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { AlbumResponseDto } from '@api';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import Button from '../elements/buttons/button.svelte';
const dispatch = createEventDispatcher<{
close: void;
updated: string;
}>();
export let album: AlbumResponseDto;
let description = album.description;
const handleSave = () => {
dispatch('updated', description);
};
</script>
<FullScreenModal on:clickOutside={() => dispatch('close')}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit description</h1>
</div>
<form on:submit|preventDefault={handleSave} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Description</label>
<!-- svelte-ignore a11y-autofocus -->
<input class="immich-form-input" id="name" name="name" type="text" bind:value={description} autofocus />
</div>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => dispatch('close')}>Cancel</Button>
<Button type="submit" fullwidth>Ok</Button>
</div>
</form>
</div>
</FullScreenModal>

View file

@ -5,6 +5,7 @@ import { userFactory } from './user-factory';
export const albumFactory = Sync.makeFactory<AlbumResponseDto>({ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
albumName: Sync.each(() => faker.commerce.product()), albumName: Sync.each(() => faker.commerce.product()),
description: '',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
assetCount: Sync.each((i) => i % 5), assetCount: Sync.each((i) => i % 5),
assets: [], assets: [],