feat: tags (#11980)

* feat: tags

* fix: folder tree icons

* navigate to tag from detail panel

* delete tag

* Tag position and add tag button

* Tag asset in detail panel

* refactor form

* feat: navigate to tag page from clicking on a tag

* feat: delete tags from the tag page

* refactor: moving tag section in detail panel and add + tag button

* feat: tag asset action in detail panel

* refactor add tag form

* fdisable add tag button when there is no selection

* feat: tag bulk endpoint

* feat: tag colors

* chore: clean up

* chore: unit tests

* feat: write tags to sidecar

* Remove tag and auto focus on tag creation form opened

* chore: regenerate migration

* chore: linting

* add color picker to tag edit form

* fix: force render tags timeline on navigating back from asset viewer

* feat: read tags from keywords

* chore: clean up

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2024-08-29 12:14:03 -04:00
committed by GitHub
parent 682adaa334
commit d08a20bd57
68 changed files with 3032 additions and 814 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map(mapTag),
tags: entity.tags?.map((tag) => mapTag(tag)),
people: peopleWithFaces(entity.faces),
unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)),
checksum: entity.checksum.toString('base64'),
+44 -18
View File
@@ -1,38 +1,64 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { TagEntity, TagType } from 'src/entities/tag.entity';
import { Optional } from 'src/validation';
import { Transform } from 'class-transformer';
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
import { TagEntity } from 'src/entities/tag.entity';
import { Optional, ValidateUUID } from 'src/validation';
export class CreateTagDto {
export class TagCreateDto {
@IsString()
@IsNotEmpty()
name!: string;
@IsEnum(TagType)
@IsNotEmpty()
@ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
type!: TagType;
@ValidateUUID({ optional: true, nullable: true })
parentId?: string | null;
@IsHexColor()
@Optional({ nullable: true, emptyToNull: true })
color?: string;
}
export class UpdateTagDto {
@IsString()
@Optional()
name?: string;
export class TagUpdateDto {
@Optional({ nullable: true, emptyToNull: true })
@IsHexColor()
@Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value))
color?: string | null;
}
export class TagUpsertDto {
@IsString({ each: true })
@IsNotEmpty({ each: true })
tags!: string[];
}
export class TagBulkAssetsDto {
@ValidateUUID({ each: true })
tagIds!: string[];
@ValidateUUID({ each: true })
assetIds!: string[];
}
export class TagBulkAssetsResponseDto {
@ApiProperty({ type: 'integer' })
count!: number;
}
export class TagResponseDto {
id!: string;
@ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
type!: string;
name!: string;
userId!: string;
value!: string;
createdAt!: Date;
updatedAt!: Date;
color?: string;
}
export function mapTag(entity: TagEntity): TagResponseDto {
return {
id: entity.id,
type: entity.type,
name: entity.name,
userId: entity.userId,
name: entity.value.split('/').at(-1) as string,
value: entity.value,
createdAt: entity.createdAt,
updatedAt: entity.updatedAt,
color: entity.color ?? undefined,
};
}
+3
View File
@@ -19,6 +19,9 @@ export class TimeBucketDto {
@ValidateUUID({ optional: true })
personId?: string;
@ValidateUUID({ optional: true })
tagId?: string;
@ValidateBoolean({ optional: true })
isArchived?: boolean;