mirror of
https://github.com/immich-app/immich.git
synced 2025-07-07 10:14:08 -04:00
feat: show archived assets for a person
This commit is contained in:
parent
e8015dc7d7
commit
882d9bee04
14
mobile/openapi/lib/api/people_api.dart
generated
14
mobile/openapi/lib/api/people_api.dart
generated
@ -184,7 +184,9 @@ class PeopleApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getPersonStatisticsWithHttpInfo(String id,) async {
|
||||
///
|
||||
/// * [bool] withArchived:
|
||||
Future<Response> getPersonStatisticsWithHttpInfo(String id, { bool? withArchived, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/people/{id}/statistics'
|
||||
.replaceAll('{id}', id);
|
||||
@ -196,6 +198,10 @@ class PeopleApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (withArchived != null) {
|
||||
queryParams.addAll(_queryParams('', 'withArchived', withArchived));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
@ -213,8 +219,10 @@ class PeopleApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<PersonStatisticsResponseDto?> getPersonStatistics(String id,) async {
|
||||
final response = await getPersonStatisticsWithHttpInfo(id,);
|
||||
///
|
||||
/// * [bool] withArchived:
|
||||
Future<PersonStatisticsResponseDto?> getPersonStatistics(String id, { bool? withArchived, }) async {
|
||||
final response = await getPersonStatisticsWithHttpInfo(id, withArchived: withArchived, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
24
mobile/openapi/lib/model/people_update_item.dart
generated
24
mobile/openapi/lib/model/people_update_item.dart
generated
@ -18,6 +18,7 @@ class PeopleUpdateItem {
|
||||
required this.id,
|
||||
this.isHidden,
|
||||
this.name,
|
||||
this.withArchived,
|
||||
});
|
||||
|
||||
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
|
||||
@ -53,13 +54,23 @@ class PeopleUpdateItem {
|
||||
///
|
||||
String? name;
|
||||
|
||||
/// This property was added in v1.118.0
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? withArchived;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateItem &&
|
||||
other.birthDate == birthDate &&
|
||||
other.featureFaceAssetId == featureFaceAssetId &&
|
||||
other.id == id &&
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name;
|
||||
other.name == name &&
|
||||
other.withArchived == withArchived;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@ -68,10 +79,11 @@ class PeopleUpdateItem {
|
||||
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isHidden == null ? 0 : isHidden!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isHidden=$isHidden, name=$name]';
|
||||
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isHidden=$isHidden, name=$name, withArchived=$withArchived]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -96,6 +108,11 @@ class PeopleUpdateItem {
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -113,6 +130,7 @@ class PeopleUpdateItem {
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden'),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
24
mobile/openapi/lib/model/person_response_dto.dart
generated
24
mobile/openapi/lib/model/person_response_dto.dart
generated
@ -19,6 +19,7 @@ class PersonResponseDto {
|
||||
required this.name,
|
||||
required this.thumbnailPath,
|
||||
this.updatedAt,
|
||||
this.withArchived,
|
||||
});
|
||||
|
||||
DateTime? birthDate;
|
||||
@ -40,6 +41,15 @@ class PersonResponseDto {
|
||||
///
|
||||
DateTime? updatedAt;
|
||||
|
||||
/// This property was added in v1.118.0
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? withArchived;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
@ -47,7 +57,8 @@ class PersonResponseDto {
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name &&
|
||||
other.thumbnailPath == thumbnailPath &&
|
||||
other.updatedAt == updatedAt;
|
||||
other.updatedAt == updatedAt &&
|
||||
other.withArchived == withArchived;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@ -57,10 +68,11 @@ class PersonResponseDto {
|
||||
(isHidden.hashCode) +
|
||||
(name.hashCode) +
|
||||
(thumbnailPath.hashCode) +
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt, withArchived=$withArchived]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -78,6 +90,11 @@ class PersonResponseDto {
|
||||
} else {
|
||||
// json[r'updatedAt'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -96,6 +113,7 @@ class PersonResponseDto {
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r''),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
24
mobile/openapi/lib/model/person_update_dto.dart
generated
24
mobile/openapi/lib/model/person_update_dto.dart
generated
@ -17,6 +17,7 @@ class PersonUpdateDto {
|
||||
this.featureFaceAssetId,
|
||||
this.isHidden,
|
||||
this.name,
|
||||
this.withArchived,
|
||||
});
|
||||
|
||||
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
|
||||
@ -49,12 +50,22 @@ class PersonUpdateDto {
|
||||
///
|
||||
String? name;
|
||||
|
||||
/// This property was added in v1.118.0
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? withArchived;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.featureFaceAssetId == featureFaceAssetId &&
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name;
|
||||
other.name == name &&
|
||||
other.withArchived == withArchived;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@ -62,10 +73,11 @@ class PersonUpdateDto {
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
|
||||
(isHidden == null ? 0 : isHidden!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden, name=$name]';
|
||||
String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden, name=$name, withArchived=$withArchived]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -89,6 +101,11 @@ class PersonUpdateDto {
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -105,6 +122,7 @@ class PersonUpdateDto {
|
||||
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden'),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -20,6 +20,7 @@ class PersonWithFacesResponseDto {
|
||||
required this.name,
|
||||
required this.thumbnailPath,
|
||||
this.updatedAt,
|
||||
this.withArchived,
|
||||
});
|
||||
|
||||
DateTime? birthDate;
|
||||
@ -43,6 +44,15 @@ class PersonWithFacesResponseDto {
|
||||
///
|
||||
DateTime? updatedAt;
|
||||
|
||||
/// This property was added in v1.118.0
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? withArchived;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
@ -51,7 +61,8 @@ class PersonWithFacesResponseDto {
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name &&
|
||||
other.thumbnailPath == thumbnailPath &&
|
||||
other.updatedAt == updatedAt;
|
||||
other.updatedAt == updatedAt &&
|
||||
other.withArchived == withArchived;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@ -62,10 +73,11 @@ class PersonWithFacesResponseDto {
|
||||
(isHidden.hashCode) +
|
||||
(name.hashCode) +
|
||||
(thumbnailPath.hashCode) +
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt, withArchived=$withArchived]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@ -84,6 +96,11 @@ class PersonWithFacesResponseDto {
|
||||
} else {
|
||||
// json[r'updatedAt'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@ -103,6 +120,7 @@ class PersonWithFacesResponseDto {
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r''),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -4152,6 +4152,14 @@
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withArchived",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@ -10097,6 +10105,10 @@
|
||||
"name": {
|
||||
"description": "Person name.",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"description": "This property was added in v1.118.0",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -10229,6 +10241,10 @@
|
||||
"description": "This property was added in v1.107.0",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"description": "This property was added in v1.118.0",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@ -10270,6 +10286,10 @@
|
||||
"name": {
|
||||
"description": "Person name.",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"description": "This property was added in v1.118.0",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@ -10303,6 +10323,10 @@
|
||||
"description": "This property was added in v1.107.0",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"description": "This property was added in v1.118.0",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -220,6 +220,8 @@ export type PersonWithFacesResponseDto = {
|
||||
thumbnailPath: string;
|
||||
/** This property was added in v1.107.0 */
|
||||
updatedAt?: string;
|
||||
/** This property was added in v1.118.0 */
|
||||
withArchived?: boolean;
|
||||
};
|
||||
export type SmartInfoResponseDto = {
|
||||
objects?: string[] | null;
|
||||
@ -502,6 +504,8 @@ export type PersonResponseDto = {
|
||||
thumbnailPath: string;
|
||||
/** This property was added in v1.107.0 */
|
||||
updatedAt?: string;
|
||||
/** This property was added in v1.118.0 */
|
||||
withArchived?: boolean;
|
||||
};
|
||||
export type AssetFaceResponseDto = {
|
||||
boundingBoxX1: number;
|
||||
@ -703,6 +707,8 @@ export type PeopleUpdateItem = {
|
||||
isHidden?: boolean;
|
||||
/** Person name. */
|
||||
name?: string;
|
||||
/** This property was added in v1.118.0 */
|
||||
withArchived?: boolean;
|
||||
};
|
||||
export type PeopleUpdateDto = {
|
||||
people: PeopleUpdateItem[];
|
||||
@ -717,6 +723,8 @@ export type PersonUpdateDto = {
|
||||
isHidden?: boolean;
|
||||
/** Person name. */
|
||||
name?: string;
|
||||
/** This property was added in v1.118.0 */
|
||||
withArchived?: boolean;
|
||||
};
|
||||
export type MergePersonDto = {
|
||||
ids: string[];
|
||||
@ -2410,13 +2418,16 @@ export function reassignFaces({ id, assetFaceUpdateDto }: {
|
||||
body: assetFaceUpdateDto
|
||||
})));
|
||||
}
|
||||
export function getPersonStatistics({ id }: {
|
||||
export function getPersonStatistics({ id, withArchived }: {
|
||||
id: string;
|
||||
withArchived?: boolean;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PersonStatisticsResponseDto;
|
||||
}>(`/people/${encodeURIComponent(id)}/statistics`, {
|
||||
}>(`/people/${encodeURIComponent(id)}/statistics${QS.query(QS.explode({
|
||||
withArchived
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
PersonResponseDto,
|
||||
PersonSearchDto,
|
||||
PersonStatisticsResponseDto,
|
||||
PersonStatsDto,
|
||||
PersonUpdateDto,
|
||||
} from 'src/dtos/person.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
@ -65,8 +66,12 @@ export class PersonController {
|
||||
|
||||
@Get(':id/statistics')
|
||||
@Authenticated({ permission: Permission.PERSON_STATISTICS })
|
||||
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
|
||||
return this.service.getStatistics(auth, id);
|
||||
getPersonStatistics(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Query() dto: PersonStatsDto,
|
||||
): Promise<PersonStatisticsResponseDto> {
|
||||
return this.service.getStatistics(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/thumbnail')
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsInt, IsNotEmpty, IsString, Max, Min, ValidateNested } from 'class-validator';
|
||||
import { IsArray, IsBoolean, IsInt, IsNotEmpty, IsString, Max, Min, ValidateNested } from 'class-validator';
|
||||
import { DateTime } from 'luxon';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@ -41,6 +41,11 @@ export class PersonUpdateDto extends PersonCreateDto {
|
||||
@Optional()
|
||||
@IsString()
|
||||
featureFaceAssetId?: string;
|
||||
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
@PropertyLifecycle({ addedAt: 'v1.118.0' })
|
||||
withArchived?: boolean;
|
||||
}
|
||||
|
||||
export class PeopleUpdateDto {
|
||||
@ -93,6 +98,8 @@ export class PersonResponseDto {
|
||||
isHidden!: boolean;
|
||||
@PropertyLifecycle({ addedAt: 'v1.107.0' })
|
||||
updatedAt?: Date;
|
||||
@PropertyLifecycle({ addedAt: 'v1.118.0' })
|
||||
withArchived?: boolean;
|
||||
}
|
||||
|
||||
export class PersonWithFacesResponseDto extends PersonResponseDto {
|
||||
@ -147,6 +154,11 @@ export class PersonStatisticsResponseDto {
|
||||
assets!: number;
|
||||
}
|
||||
|
||||
export class PersonStatsDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
withArchived?: boolean;
|
||||
}
|
||||
|
||||
export class PeopleResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
total!: number;
|
||||
@ -167,6 +179,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: person.isHidden,
|
||||
updatedAt: person.updatedAt,
|
||||
withArchived: person.withArchived,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -49,4 +49,7 @@ export class PersonEntity {
|
||||
|
||||
@Column({ default: false })
|
||||
isHidden!: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
withArchived!: boolean;
|
||||
}
|
||||
|
@ -45,6 +45,10 @@ export interface DeleteFacesOptions {
|
||||
sourceType: SourceType;
|
||||
}
|
||||
|
||||
export interface PersonStatsOptions {
|
||||
withArchived?: boolean;
|
||||
}
|
||||
|
||||
export type UnassignFacesOptions = DeleteFacesOptions;
|
||||
|
||||
export interface IPersonRepository {
|
||||
@ -74,7 +78,7 @@ export interface IPersonRepository {
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]>;
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
|
||||
getStatistics(personId: string): Promise<PersonStatistics>;
|
||||
getStatistics(personId: string, options: PersonStatsOptions): Promise<PersonStatistics>;
|
||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddWithArchivedToPerson1728944141526 implements MigrationInterface {
|
||||
name = 'AddWithArchivedToPerson1728944141526'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" ADD "withArchived" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "withArchived"`);
|
||||
}
|
||||
|
||||
}
|
@ -212,6 +212,7 @@ SELECT
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath",
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."withArchived" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_withArchived",
|
||||
"AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id",
|
||||
"AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId",
|
||||
"AssetEntity__AssetEntity_stack"."primaryAssetId" AS "AssetEntity__AssetEntity_stack_primaryAssetId",
|
||||
|
@ -17,7 +17,8 @@ SELECT
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
"person"."isHidden" AS "person_isHidden",
|
||||
"person"."withArchived" AS "person_withArchived"
|
||||
FROM
|
||||
"person" "person"
|
||||
LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
@ -54,7 +55,8 @@ SELECT
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
"person"."isHidden" AS "person_isHidden",
|
||||
"person"."withArchived" AS "person_withArchived"
|
||||
FROM
|
||||
"person" "person"
|
||||
LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
@ -83,7 +85,8 @@ SELECT
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."withArchived" AS "AssetFaceEntity__AssetFaceEntity_person_withArchived"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
@ -116,7 +119,8 @@ FROM
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."withArchived" AS "AssetFaceEntity__AssetFaceEntity_person_withArchived"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
@ -153,6 +157,7 @@ FROM
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."withArchived" AS "AssetFaceEntity__AssetFaceEntity_person_withArchived",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
||||
@ -213,7 +218,8 @@ SELECT
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
"person"."isHidden" AS "person_isHidden",
|
||||
"person"."withArchived" AS "person_withArchived"
|
||||
FROM
|
||||
"person" "person"
|
||||
WHERE
|
||||
@ -242,11 +248,18 @@ FROM
|
||||
"asset_faces" "face"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
LEFT JOIN "person" "person" ON "person"."id" = "face"."personId"
|
||||
WHERE
|
||||
"face"."personId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
AND "asset"."deletedAt" IS NULL
|
||||
AND "asset"."livePhotoVideoId" IS NULL
|
||||
AND (
|
||||
(
|
||||
"person"."withArchived" = false
|
||||
AND "asset"."isArchived" = false
|
||||
)
|
||||
OR "person"."withArchived" = true
|
||||
)
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
SELECT
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
PersonNameSearchOptions,
|
||||
PersonSearchOptions,
|
||||
PersonStatistics,
|
||||
PersonStatsOptions,
|
||||
UnassignFacesOptions,
|
||||
UpdateFacesData,
|
||||
} from 'src/interfaces/person.interface';
|
||||
@ -214,17 +215,33 @@ export class PersonRepository implements IPersonRepository {
|
||||
return queryBuilder.getMany();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getStatistics(personId: string): Promise<PersonStatistics> {
|
||||
const items = await this.assetFaceRepository
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withArchived: undefined }] })
|
||||
async getStatistics(personId: string, options: PersonStatsOptions): Promise<PersonStatistics> {
|
||||
/*
|
||||
* withArchived: true -> Return the count of all assets for a given person
|
||||
* withArchived: false -> Return the count of all unarchived assets for a given person
|
||||
* withArchived: undefiend ->
|
||||
* - If person.withArchived = true -> Return the count of all assets for a given person
|
||||
* - If person.withArchived = false -> Return the count of all unarchived assets for a given person
|
||||
*/
|
||||
|
||||
let queryBuilder = this.assetFaceRepository
|
||||
.createQueryBuilder('face')
|
||||
.leftJoin('face.asset', 'asset')
|
||||
.where('face.personId = :personId', { personId })
|
||||
.andWhere('asset.isArchived = false')
|
||||
.andWhere('asset.deletedAt IS NULL')
|
||||
.andWhere('asset.livePhotoVideoId IS NULL')
|
||||
.select('COUNT(DISTINCT(asset.id))', 'count')
|
||||
.getRawOne();
|
||||
.select('COUNT(DISTINCT(asset.id))', 'count');
|
||||
|
||||
if (options.withArchived === false) {
|
||||
queryBuilder = queryBuilder.andWhere('asset.isArchived = false');
|
||||
} else if (options.withArchived === undefined) {
|
||||
queryBuilder = queryBuilder
|
||||
.leftJoin('face.person', 'person')
|
||||
.andWhere('((person.withArchived = false AND asset.isArchived = false) OR person.withArchived = true)');
|
||||
}
|
||||
|
||||
const items = await queryBuilder.getRawOne();
|
||||
return {
|
||||
assets: items.count ?? 0,
|
||||
};
|
||||
|
@ -31,6 +31,7 @@ const responseDto: PersonResponseDto = {
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
isHidden: false,
|
||||
updatedAt: expect.any(Date),
|
||||
withArchived: false,
|
||||
};
|
||||
|
||||
const statistics = { assets: 3 };
|
||||
@ -118,6 +119,7 @@ describe(PersonService.name, () => {
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
isHidden: true,
|
||||
updatedAt: expect.any(Date),
|
||||
withArchived: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -218,6 +220,16 @@ describe(PersonService.name, () => {
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it("should update a person's withArchived", async () => {
|
||||
personMock.update.mockResolvedValue(personStub.withName);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
|
||||
await expect(sut.update(authStub.admin, 'person-1', { withArchived: true })).resolves.toEqual(responseDto);
|
||||
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', withArchived: true });
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it("should update a person's date of birth", async () => {
|
||||
personMock.update.mockResolvedValue(personStub.withBirthDate);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
@ -229,6 +241,7 @@ describe(PersonService.name, () => {
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
isHidden: false,
|
||||
updatedAt: expect.any(Date),
|
||||
withArchived: false,
|
||||
});
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '1976-06-30' });
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
@ -381,6 +394,7 @@ describe(PersonService.name, () => {
|
||||
name: personStub.noName.name,
|
||||
thumbnailPath: personStub.noName.thumbnailPath,
|
||||
updatedAt: expect.any(Date),
|
||||
withArchived: personStub.noName.withArchived,
|
||||
});
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalledWith();
|
||||
@ -1171,13 +1185,13 @@ describe(PersonService.name, () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getStatistics.mockResolvedValue(statistics);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 });
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1', {})).resolves.toEqual({ assets: 3 });
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should require person.read permission', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
});
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
PersonResponseDto,
|
||||
PersonSearchDto,
|
||||
PersonStatisticsResponseDto,
|
||||
PersonStatsDto,
|
||||
PersonUpdateDto,
|
||||
mapFaces,
|
||||
mapPerson,
|
||||
@ -153,9 +154,9 @@ export class PersonService extends BaseService {
|
||||
return this.findOrFail(id).then(mapPerson);
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||
async getStatistics(auth: AuthDto, id: string, dto: PersonStatsDto): Promise<PersonStatisticsResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
return this.personRepository.getStatistics(id);
|
||||
return this.personRepository.getStatistics(id, dto);
|
||||
}
|
||||
|
||||
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
@ -184,7 +185,7 @@ export class PersonService extends BaseService {
|
||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId, withArchived } = dto;
|
||||
// TODO: set by faceId directly
|
||||
let faceId: string | undefined = undefined;
|
||||
if (assetId) {
|
||||
@ -197,7 +198,14 @@ export class PersonService extends BaseService {
|
||||
faceId = face.id;
|
||||
}
|
||||
|
||||
const person = await this.personRepository.update({ id, faceAssetId: faceId, name, birthDate, isHidden });
|
||||
const person = await this.personRepository.update({
|
||||
id,
|
||||
faceAssetId: faceId,
|
||||
name,
|
||||
birthDate,
|
||||
isHidden,
|
||||
withArchived,
|
||||
});
|
||||
|
||||
if (assetId) {
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
||||
|
9
server/test/fixtures/person.stub.ts
vendored
9
server/test/fixtures/person.stub.ts
vendored
@ -15,6 +15,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
hidden: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@ -29,6 +30,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: true,
|
||||
withArchived: false,
|
||||
}),
|
||||
withName: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@ -43,6 +45,7 @@ export const personStub = {
|
||||
faceAssetId: 'assetFaceId',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
withBirthDate: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@ -57,6 +60,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
noThumbnail: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@ -71,6 +75,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
newThumbnail: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@ -85,6 +90,7 @@ export const personStub = {
|
||||
faceAssetId: 'asset-id',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
primaryPerson: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@ -99,6 +105,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
mergePerson: Object.freeze<PersonEntity>({
|
||||
id: 'person-2',
|
||||
@ -113,6 +120,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
randomPerson: Object.freeze<PersonEntity>({
|
||||
id: 'person-3',
|
||||
@ -127,5 +135,6 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
};
|
||||
|
@ -1140,6 +1140,7 @@
|
||||
"show_albums": "Show albums",
|
||||
"show_all_people": "Show all people",
|
||||
"show_and_hide_people": "Show & hide people",
|
||||
"show_archived_or_unarchived_assets": "{with, select, true {With} other {Without}} archived assets",
|
||||
"show_file_location": "Show file location",
|
||||
"show_gallery": "Show gallery",
|
||||
"show_hidden_people": "Show hidden people",
|
||||
|
@ -45,6 +45,8 @@
|
||||
import {
|
||||
mdiAccountBoxOutline,
|
||||
mdiAccountMultipleCheckOutline,
|
||||
mdiArchiveArrowDown,
|
||||
mdiArchiveArrowDownOutline,
|
||||
mdiArrowLeft,
|
||||
mdiCalendarEditOutline,
|
||||
mdiDotsVertical,
|
||||
@ -72,8 +74,10 @@
|
||||
UNASSIGN_ASSETS = 'unassign-faces',
|
||||
}
|
||||
|
||||
$: isArchived = person.withArchived ? undefined : person.withArchived;
|
||||
|
||||
let assetStore = new AssetStore({
|
||||
isArchived: false,
|
||||
isArchived,
|
||||
personId: data.person.id,
|
||||
});
|
||||
|
||||
@ -81,7 +85,7 @@
|
||||
$: thumbnailData = getPeopleThumbnailUrl(person);
|
||||
$: if (person) {
|
||||
handlePromiseError(updateAssetCount());
|
||||
handlePromiseError(assetStore.updateOptions({ personId: person.id }));
|
||||
handlePromiseError(assetStore.updateOptions({ personId: person.id, isArchived }));
|
||||
}
|
||||
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
@ -338,6 +342,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleWithArhived = async () => {
|
||||
const withArchived = !person.withArchived;
|
||||
|
||||
try {
|
||||
await updatePerson({
|
||||
id: person.id,
|
||||
personUpdateDto: { withArchived },
|
||||
});
|
||||
|
||||
data.person.withArchived = withArchived;
|
||||
refreshAssetGrid = !refreshAssetGrid;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_archive_unarchive', { values: { archived: withArchived } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAssets = async (assetIds: string[]) => {
|
||||
$assetStore.removeAssets(assetIds);
|
||||
await updateAssetCount();
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
@ -395,7 +420,7 @@
|
||||
<ChangeDate menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem unarchive={isAllArchive} onArchive={(assetIds) => $assetStore.removeAssets(assetIds)} />
|
||||
<DeleteAssets menuItem onAssetDelete={(assetIds) => $assetStore.removeAssets(assetIds)} />
|
||||
<DeleteAssets menuItem onAssetDelete={handleDeleteAssets} />
|
||||
</ButtonContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
@ -423,6 +448,11 @@
|
||||
icon={mdiAccountMultipleCheckOutline}
|
||||
onClick={() => (viewMode = ViewMode.MERGE_PEOPLE)}
|
||||
/>
|
||||
<MenuOption
|
||||
text={$t('show_archived_or_unarchived_assets', { values: { with: !person.withArchived } })}
|
||||
icon={person.withArchived ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
|
||||
onClick={handleToggleWithArhived}
|
||||
/>
|
||||
</ButtonContextMenu>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
|
Loading…
x
Reference in New Issue
Block a user