fix: hide faces (#3352)

* fix: hide faces

* remove unused variable

* fix: work even if one fails

* better style for hidden people

* add hide face in the menu dropdown

* add buttons to toggle visibility for all faces

* add server test

* close modal with escape key

* fix: explore page

* improve show & hide faces modal

* keep name on people card

* simplify layout

* sticky app bar in show-hide page

* fix format

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
martin
2023-07-23 05:00:43 +02:00
committed by GitHub
parent c40aa4399b
commit ed64c91da6
25 changed files with 1097 additions and 72 deletions

View File

@@ -2546,6 +2546,49 @@
"api_key": []
}
]
},
"put": {
"operationId": "updatePeople",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PeopleUpdateDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BulkIdResponseDto"
}
}
}
}
}
},
"tags": [
"Person"
],
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
]
}
},
"/person/{id}": {
@@ -5028,13 +5071,13 @@
"type": "boolean"
},
"error": {
"type": "string",
"enum": [
"duplicate",
"no_permission",
"not_found",
"unknown"
]
],
"type": "string"
}
},
"required": [
@@ -5906,6 +5949,44 @@
"people"
]
},
"PeopleUpdateDto": {
"type": "object",
"properties": {
"people": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PeopleUpdateItem"
}
}
},
"required": [
"people"
]
},
"PeopleUpdateItem": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Person id."
},
"name": {
"type": "string",
"description": "Person name."
},
"featureFaceAssetId": {
"type": "string",
"description": "Asset is used to get the feature face thumbnail."
},
"isHidden": {
"type": "boolean",
"description": "Person visibility"
}
},
"required": [
"id"
]
},
"PersonResponseDto": {
"type": "object",
"properties": {

View File

@@ -1,6 +1,6 @@
import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional, IsString } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
import { toBoolean, ValidateUUID } from '../domain.util';
export class PersonUpdateDto {
@@ -26,6 +26,43 @@ export class PersonUpdateDto {
isHidden?: boolean;
}
export class PeopleUpdateDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => PeopleUpdateItem)
people!: PeopleUpdateItem[];
}
export class PeopleUpdateItem {
/**
* Person id.
*/
@IsString()
@IsNotEmpty()
id!: string;
/**
* Person name.
*/
@IsOptional()
@IsString()
name?: string;
/**
* Asset is used to get the feature face thumbnail.
*/
@IsOptional()
@IsString()
featureFaceAssetId?: string;
/**
* Person visibility
*/
@IsOptional()
@IsBoolean()
isHidden?: boolean;
}
export class MergePersonDto {
@ValidateUUID({ each: true })
ids!: string[];

View File

@@ -188,6 +188,16 @@ describe(PersonService.name, () => {
});
});
describe('updateAll', () => {
it('should throw an error when personId is invalid', async () => {
personMock.getById.mockResolvedValue(null);
await expect(
sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }),
).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]);
expect(personMock.update).not.toHaveBeenCalled();
});
});
describe('handlePersonCleanup', () => {
it('should delete people without faces', async () => {
personMock.getAllWithoutFaces.mockResolvedValue([personStub.noName]);

View File

@@ -8,6 +8,7 @@ import {
mapPerson,
MergePersonDto,
PeopleResponseDto,
PeopleUpdateDto,
PersonResponseDto,
PersonSearchDto,
PersonUpdateDto,
@@ -96,6 +97,24 @@ export class PersonService {
return mapPerson(person);
}
async updatePeople(authUser: AuthUserDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
const results: BulkIdResponseDto[] = [];
for (const person of dto.people) {
try {
await this.update(authUser, person.id, {
isHidden: person.isHidden,
name: person.name,
featureFaceAssetId: person.featureFaceAssetId,
}),
results.push({ id: person.id, success: true });
} catch (error: Error | any) {
this.logger.error(`Unable to update ${person.id} : ${error}`, error?.stack);
results.push({ id: person.id, success: false, error: BulkIdErrorReason.UNKNOWN });
}
}
return results;
}
async handlePersonCleanup() {
const people = await this.repository.getAllWithoutFaces();
for (const person of people) {

View File

@@ -5,6 +5,7 @@ import {
ImmichReadStream,
MergePersonDto,
PeopleResponseDto,
PeopleUpdateDto,
PersonResponseDto,
PersonSearchDto,
PersonService,
@@ -32,6 +33,11 @@ export class PersonController {
return this.service.getAll(authUser, withHidden);
}
@Put()
updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.updatePeople(authUser, dto);
}
@Get(':id')
getPerson(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
return this.service.getById(authUser, id);