feat(api): set person color (#15937)

This commit is contained in:
Jason Rasmussen 2025-02-07 10:06:58 -05:00 committed by GitHub
parent 2e5007adef
commit 23014c263b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 182 additions and 21 deletions

View File

@ -195,6 +195,7 @@ describe('/people', () => {
.send({ .send({
name: 'New Person', name: 'New Person',
birthDate: '1990-01-01', birthDate: '1990-01-01',
color: '#333',
}); });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toMatchObject({ expect(body).toMatchObject({
@ -273,6 +274,24 @@ describe('/people', () => {
expect(body).toMatchObject({ birthDate: null }); expect(body).toMatchObject({ birthDate: null });
}); });
it('should set a color', async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ color: '#555' });
expect(status).toBe(200);
expect(body).toMatchObject({ color: '#555' });
});
it('should clear a color', async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ color: null });
expect(status).toBe(200);
expect(body.color).toBeUndefined();
});
it('should mark a person as favorite', async () => { it('should mark a person as favorite', async () => {
const person = await utils.createPerson(admin.accessToken, { const person = await utils.createPerson(admin.accessToken, {
name: 'visible_person', name: 'visible_person',

View File

@ -14,6 +14,7 @@ class PeopleUpdateItem {
/// Returns a new [PeopleUpdateItem] instance. /// Returns a new [PeopleUpdateItem] instance.
PeopleUpdateItem({ PeopleUpdateItem({
this.birthDate, this.birthDate,
this.color,
this.featureFaceAssetId, this.featureFaceAssetId,
required this.id, required this.id,
this.isFavorite, this.isFavorite,
@ -24,6 +25,8 @@ class PeopleUpdateItem {
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null. /// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate; DateTime? birthDate;
String? color;
/// Asset is used to get the feature face thumbnail. /// Asset is used to get the feature face thumbnail.
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@ -65,6 +68,7 @@ class PeopleUpdateItem {
@override @override
bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateItem && bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateItem &&
other.birthDate == birthDate && other.birthDate == birthDate &&
other.color == color &&
other.featureFaceAssetId == featureFaceAssetId && other.featureFaceAssetId == featureFaceAssetId &&
other.id == id && other.id == id &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
@ -75,6 +79,7 @@ class PeopleUpdateItem {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) + (birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) + (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
(id.hashCode) + (id.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
@ -82,7 +87,7 @@ class PeopleUpdateItem {
(name == null ? 0 : name!.hashCode); (name == null ? 0 : name!.hashCode);
@override @override
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]'; String toString() => 'PeopleUpdateItem[birthDate=$birthDate, color=$color, featureFaceAssetId=$featureFaceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -91,6 +96,11 @@ class PeopleUpdateItem {
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
} }
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
if (this.featureFaceAssetId != null) { if (this.featureFaceAssetId != null) {
json[r'featureFaceAssetId'] = this.featureFaceAssetId; json[r'featureFaceAssetId'] = this.featureFaceAssetId;
} else { } else {
@ -125,6 +135,7 @@ class PeopleUpdateItem {
return PeopleUpdateItem( return PeopleUpdateItem(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'), featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),

View File

@ -14,6 +14,7 @@ class PersonCreateDto {
/// Returns a new [PersonCreateDto] instance. /// Returns a new [PersonCreateDto] instance.
PersonCreateDto({ PersonCreateDto({
this.birthDate, this.birthDate,
this.color,
this.isFavorite, this.isFavorite,
this.isHidden, this.isHidden,
this.name, this.name,
@ -22,6 +23,8 @@ class PersonCreateDto {
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null. /// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate; DateTime? birthDate;
String? color;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// 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 /// does not include a default value (using the "default:" property), however, the generated
@ -51,6 +54,7 @@ class PersonCreateDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is PersonCreateDto && bool operator ==(Object other) => identical(this, other) || other is PersonCreateDto &&
other.birthDate == birthDate && other.birthDate == birthDate &&
other.color == color &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.isHidden == isHidden && other.isHidden == isHidden &&
other.name == name; other.name == name;
@ -59,12 +63,13 @@ class PersonCreateDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) + (birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(isHidden == null ? 0 : isHidden!.hashCode) + (isHidden == null ? 0 : isHidden!.hashCode) +
(name == null ? 0 : name!.hashCode); (name == null ? 0 : name!.hashCode);
@override @override
String toString() => 'PersonCreateDto[birthDate=$birthDate, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]'; String toString() => 'PersonCreateDto[birthDate=$birthDate, color=$color, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -73,6 +78,11 @@ class PersonCreateDto {
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
} }
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
if (this.isFavorite != null) { if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite; json[r'isFavorite'] = this.isFavorite;
} else { } else {
@ -101,6 +111,7 @@ class PersonCreateDto {
return PersonCreateDto( return PersonCreateDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden'), isHidden: mapValueOfType<bool>(json, r'isHidden'),
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),

View File

@ -14,6 +14,7 @@ class PersonResponseDto {
/// Returns a new [PersonResponseDto] instance. /// Returns a new [PersonResponseDto] instance.
PersonResponseDto({ PersonResponseDto({
required this.birthDate, required this.birthDate,
this.color,
required this.id, required this.id,
this.isFavorite, this.isFavorite,
required this.isHidden, required this.isHidden,
@ -24,6 +25,15 @@ class PersonResponseDto {
DateTime? birthDate; DateTime? birthDate;
/// This property was added in v1.126.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.
///
String? color;
String id; String id;
/// This property was added in v1.126.0 /// This property was added in v1.126.0
@ -53,6 +63,7 @@ class PersonResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto && bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
other.birthDate == birthDate && other.birthDate == birthDate &&
other.color == color &&
other.id == id && other.id == id &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.isHidden == isHidden && other.isHidden == isHidden &&
@ -64,6 +75,7 @@ class PersonResponseDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) + (birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(id.hashCode) + (id.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(isHidden.hashCode) + (isHidden.hashCode) +
@ -72,7 +84,7 @@ class PersonResponseDto {
(updatedAt == null ? 0 : updatedAt!.hashCode); (updatedAt == null ? 0 : updatedAt!.hashCode);
@override @override
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]'; String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -80,6 +92,11 @@ class PersonResponseDto {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
}
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
} }
json[r'id'] = this.id; json[r'id'] = this.id;
if (this.isFavorite != null) { if (this.isFavorite != null) {
@ -108,6 +125,7 @@ class PersonResponseDto {
return PersonResponseDto( return PersonResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden')!, isHidden: mapValueOfType<bool>(json, r'isHidden')!,

View File

@ -14,6 +14,7 @@ class PersonUpdateDto {
/// Returns a new [PersonUpdateDto] instance. /// Returns a new [PersonUpdateDto] instance.
PersonUpdateDto({ PersonUpdateDto({
this.birthDate, this.birthDate,
this.color,
this.featureFaceAssetId, this.featureFaceAssetId,
this.isFavorite, this.isFavorite,
this.isHidden, this.isHidden,
@ -23,6 +24,8 @@ class PersonUpdateDto {
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null. /// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate; DateTime? birthDate;
String? color;
/// Asset is used to get the feature face thumbnail. /// Asset is used to get the feature face thumbnail.
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@ -61,6 +64,7 @@ class PersonUpdateDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto && bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto &&
other.birthDate == birthDate && other.birthDate == birthDate &&
other.color == color &&
other.featureFaceAssetId == featureFaceAssetId && other.featureFaceAssetId == featureFaceAssetId &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.isHidden == isHidden && other.isHidden == isHidden &&
@ -70,13 +74,14 @@ class PersonUpdateDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) + (birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) + (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(isHidden == null ? 0 : isHidden!.hashCode) + (isHidden == null ? 0 : isHidden!.hashCode) +
(name == null ? 0 : name!.hashCode); (name == null ? 0 : name!.hashCode);
@override @override
String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]'; String toString() => 'PersonUpdateDto[birthDate=$birthDate, color=$color, featureFaceAssetId=$featureFaceAssetId, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -85,6 +90,11 @@ class PersonUpdateDto {
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
} }
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
if (this.featureFaceAssetId != null) { if (this.featureFaceAssetId != null) {
json[r'featureFaceAssetId'] = this.featureFaceAssetId; json[r'featureFaceAssetId'] = this.featureFaceAssetId;
} else { } else {
@ -118,6 +128,7 @@ class PersonUpdateDto {
return PersonUpdateDto( return PersonUpdateDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'), featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden'), isHidden: mapValueOfType<bool>(json, r'isHidden'),

View File

@ -14,6 +14,7 @@ class PersonWithFacesResponseDto {
/// Returns a new [PersonWithFacesResponseDto] instance. /// Returns a new [PersonWithFacesResponseDto] instance.
PersonWithFacesResponseDto({ PersonWithFacesResponseDto({
required this.birthDate, required this.birthDate,
this.color,
this.faces = const [], this.faces = const [],
required this.id, required this.id,
this.isFavorite, this.isFavorite,
@ -25,6 +26,15 @@ class PersonWithFacesResponseDto {
DateTime? birthDate; DateTime? birthDate;
/// This property was added in v1.126.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.
///
String? color;
List<AssetFaceWithoutPersonResponseDto> faces; List<AssetFaceWithoutPersonResponseDto> faces;
String id; String id;
@ -56,6 +66,7 @@ class PersonWithFacesResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto && bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
other.birthDate == birthDate && other.birthDate == birthDate &&
other.color == color &&
_deepEquality.equals(other.faces, faces) && _deepEquality.equals(other.faces, faces) &&
other.id == id && other.id == id &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
@ -68,6 +79,7 @@ class PersonWithFacesResponseDto {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) + (birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(faces.hashCode) + (faces.hashCode) +
(id.hashCode) + (id.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
@ -77,7 +89,7 @@ class PersonWithFacesResponseDto {
(updatedAt == null ? 0 : updatedAt!.hashCode); (updatedAt == null ? 0 : updatedAt!.hashCode);
@override @override
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]'; String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, color=$color, faces=$faces, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -85,6 +97,11 @@ class PersonWithFacesResponseDto {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
} else { } else {
// json[r'birthDate'] = null; // json[r'birthDate'] = null;
}
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
} }
json[r'faces'] = this.faces; json[r'faces'] = this.faces;
json[r'id'] = this.id; json[r'id'] = this.id;
@ -114,6 +131,7 @@ class PersonWithFacesResponseDto {
return PersonWithFacesResponseDto( return PersonWithFacesResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']), faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),

View File

@ -10286,6 +10286,10 @@
"nullable": true, "nullable": true,
"type": "string" "type": "string"
}, },
"color": {
"nullable": true,
"type": "string"
},
"featureFaceAssetId": { "featureFaceAssetId": {
"description": "Asset is used to get the feature face thumbnail.", "description": "Asset is used to get the feature face thumbnail.",
"type": "string" "type": "string"
@ -10402,6 +10406,10 @@
"nullable": true, "nullable": true,
"type": "string" "type": "string"
}, },
"color": {
"nullable": true,
"type": "string"
},
"isFavorite": { "isFavorite": {
"type": "boolean" "type": "boolean"
}, },
@ -10423,6 +10431,10 @@
"nullable": true, "nullable": true,
"type": "string" "type": "string"
}, },
"color": {
"description": "This property was added in v1.126.0",
"type": "string"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@ -10473,6 +10485,10 @@
"nullable": true, "nullable": true,
"type": "string" "type": "string"
}, },
"color": {
"nullable": true,
"type": "string"
},
"featureFaceAssetId": { "featureFaceAssetId": {
"description": "Asset is used to get the feature face thumbnail.", "description": "Asset is used to get the feature face thumbnail.",
"type": "string" "type": "string"
@ -10498,6 +10514,10 @@
"nullable": true, "nullable": true,
"type": "string" "type": "string"
}, },
"color": {
"description": "This property was added in v1.126.0",
"type": "string"
},
"faces": { "faces": {
"items": { "items": {
"$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto" "$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto"
@ -12611,7 +12631,6 @@
"properties": { "properties": {
"color": { "color": {
"nullable": true, "nullable": true,
"pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
"type": "string" "type": "string"
} }
}, },

View File

@ -213,6 +213,8 @@ export type AssetFaceWithoutPersonResponseDto = {
}; };
export type PersonWithFacesResponseDto = { export type PersonWithFacesResponseDto = {
birthDate: string | null; birthDate: string | null;
/** This property was added in v1.126.0 */
color?: string;
faces: AssetFaceWithoutPersonResponseDto[]; faces: AssetFaceWithoutPersonResponseDto[];
id: string; id: string;
/** This property was added in v1.126.0 */ /** This property was added in v1.126.0 */
@ -493,6 +495,8 @@ export type DuplicateResponseDto = {
}; };
export type PersonResponseDto = { export type PersonResponseDto = {
birthDate: string | null; birthDate: string | null;
/** This property was added in v1.126.0 */
color?: string;
id: string; id: string;
/** This property was added in v1.126.0 */ /** This property was added in v1.126.0 */
isFavorite?: boolean; isFavorite?: boolean;
@ -693,6 +697,7 @@ export type PersonCreateDto = {
/** Person date of birth. /** Person date of birth.
Note: the mobile app cannot currently set the birth date to null. */ Note: the mobile app cannot currently set the birth date to null. */
birthDate?: string | null; birthDate?: string | null;
color?: string | null;
isFavorite?: boolean; isFavorite?: boolean;
/** Person visibility */ /** Person visibility */
isHidden?: boolean; isHidden?: boolean;
@ -703,6 +708,7 @@ export type PeopleUpdateItem = {
/** Person date of birth. /** Person date of birth.
Note: the mobile app cannot currently set the birth date to null. */ Note: the mobile app cannot currently set the birth date to null. */
birthDate?: string | null; birthDate?: string | null;
color?: string | null;
/** Asset is used to get the feature face thumbnail. */ /** Asset is used to get the feature face thumbnail. */
featureFaceAssetId?: string; featureFaceAssetId?: string;
/** Person id. */ /** Person id. */
@ -720,6 +726,7 @@ export type PersonUpdateDto = {
/** Person date of birth. /** Person date of birth.
Note: the mobile app cannot currently set the birth date to null. */ Note: the mobile app cannot currently set the birth date to null. */
birthDate?: string | null; birthDate?: string | null;
color?: string | null;
/** Asset is used to get the feature face thumbnail. */ /** Asset is used to get the feature face thumbnail. */
featureFaceAssetId?: string; featureFaceAssetId?: string;
isFavorite?: boolean; isFavorite?: boolean;

1
server/src/db.d.ts vendored
View File

@ -276,6 +276,7 @@ export interface Partners {
export interface Person { export interface Person {
birthDate: Timestamp | null; birthDate: Timestamp | null;
color: string | null;
createdAt: Generated<Timestamp>; createdAt: Generated<Timestamp>;
faceAssetId: string | null; faceAssetId: string | null;
id: Generated<string>; id: Generated<string>;

View File

@ -7,7 +7,14 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { SourceType } from 'src/enum'; import { SourceType } from 'src/enum';
import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; import {
IsDateStringFormat,
MaxDateString,
Optional,
ValidateBoolean,
ValidateHexColor,
ValidateUUID,
} from 'src/validation';
export class PersonCreateDto { export class PersonCreateDto {
/** /**
@ -35,6 +42,10 @@ export class PersonCreateDto {
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isFavorite?: boolean; isFavorite?: boolean;
@Optional({ emptyToNull: true, nullable: true })
@ValidateHexColor()
color?: string | null;
} }
export class PersonUpdateDto extends PersonCreateDto { export class PersonUpdateDto extends PersonCreateDto {
@ -102,6 +113,8 @@ export class PersonResponseDto {
updatedAt?: Date; updatedAt?: Date;
@PropertyLifecycle({ addedAt: 'v1.126.0' }) @PropertyLifecycle({ addedAt: 'v1.126.0' })
isFavorite?: boolean; isFavorite?: boolean;
@PropertyLifecycle({ addedAt: 'v1.126.0' })
color?: string;
} }
export class PersonWithFacesResponseDto extends PersonResponseDto { export class PersonWithFacesResponseDto extends PersonResponseDto {
@ -176,6 +189,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
thumbnailPath: person.thumbnailPath, thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden, isHidden: person.isHidden,
isFavorite: person.isFavorite, isFavorite: person.isFavorite,
color: person.color ?? undefined,
updatedAt: person.updatedAt, updatedAt: person.updatedAt,
}; };
} }

View File

@ -1,8 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
import { TagEntity } from 'src/entities/tag.entity'; import { TagEntity } from 'src/entities/tag.entity';
import { Optional, ValidateUUID } from 'src/validation'; import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
export class TagCreateDto { export class TagCreateDto {
@IsString() @IsString()
@ -18,9 +17,8 @@ export class TagCreateDto {
} }
export class TagUpdateDto { export class TagUpdateDto {
@Optional({ nullable: true, emptyToNull: true }) @Optional({ emptyToNull: true, nullable: true })
@IsHexColor() @ValidateHexColor()
@Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value))
color?: string | null; color?: string | null;
} }

View File

@ -52,4 +52,7 @@ export class PersonEntity {
@Column({ default: false }) @Column({ default: false })
isFavorite!: boolean; isFavorite!: boolean;
@Column({ type: 'varchar', nullable: true, default: null })
color?: string | null;
} }

View File

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddPersonColor1738889177573 implements MigrationInterface {
name = 'AddPersonColor1738889177573'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "person" ADD "color" character varying`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "color"`);
}
}

View File

@ -355,7 +355,7 @@ describe(PersonService.name, () => {
sut.reassignFaces(authStub.admin, personStub.noName.id, { sut.reassignFaces(authStub.admin, personStub.noName.id, {
data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }], data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }],
}), }),
).resolves.toEqual([personStub.noName]); ).resolves.toBeDefined();
expect(jobMock.queueAll).toHaveBeenCalledWith([ expect(jobMock.queueAll).toHaveBeenCalledWith([
{ {
@ -448,7 +448,7 @@ describe(PersonService.name, () => {
it('should create a new person', async () => { it('should create a new person', async () => {
personMock.create.mockResolvedValue(personStub.primaryPerson); personMock.create.mockResolvedValue(personStub.primaryPerson);
await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson); await expect(sut.create(authStub.admin, {})).resolves.toBeDefined();
expect(personMock.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id }); expect(personMock.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id });
}); });

View File

@ -104,7 +104,7 @@ export class PersonService extends BaseService {
await this.personRepository.reassignFace(face.id, personId); await this.personRepository.reassignFace(face.id, personId);
} }
result.push(person); result.push(mapPerson(person));
} }
if (changeFeaturePhoto.length > 0) { if (changeFeaturePhoto.length > 0) {
// Remove duplicates // Remove duplicates
@ -178,20 +178,23 @@ export class PersonService extends BaseService {
}); });
} }
create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> { async create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
return this.personRepository.create({ const person = await this.personRepository.create({
ownerId: auth.user.id, ownerId: auth.user.id,
name: dto.name, name: dto.name,
birthDate: dto.birthDate, birthDate: dto.birthDate,
isHidden: dto.isHidden, isHidden: dto.isHidden,
isFavorite: dto.isFavorite, isFavorite: dto.isFavorite,
color: dto.color,
}); });
return mapPerson(person);
} }
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> { async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite } = dto; const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto;
// TODO: set by faceId directly // TODO: set by faceId directly
let faceId: string | undefined = undefined; let faceId: string | undefined = undefined;
if (assetId) { if (assetId) {
@ -211,6 +214,7 @@ export class PersonService extends BaseService {
birthDate, birthDate,
isHidden, isHidden,
isFavorite, isFavorite,
color,
}); });
if (assetId) { if (assetId) {

View File

@ -31,6 +31,8 @@ describe(SearchService.name, () => {
it('should pass options to search', async () => { it('should pass options to search', async () => {
const { name } = personStub.withName; const { name } = personStub.withName;
personMock.getByName.mockResolvedValue([]);
await sut.searchPerson(authStub.user1, { name, withHidden: false }); await sut.searchPerson(authStub.user1, { name, withHidden: false });
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false }); expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false });

View File

@ -1,8 +1,9 @@
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { PersonResponseDto } from 'src/dtos/person.dto'; import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
import { import {
mapPlaces,
MetadataSearchDto, MetadataSearchDto,
PlacesResponseDto, PlacesResponseDto,
RandomSearchDto, RandomSearchDto,
@ -12,7 +13,6 @@ import {
SearchSuggestionRequestDto, SearchSuggestionRequestDto,
SearchSuggestionType, SearchSuggestionType,
SmartSearchDto, SmartSearchDto,
mapPlaces,
} from 'src/dtos/search.dto'; } from 'src/dtos/search.dto';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
import { AssetOrder } from 'src/enum'; import { AssetOrder } from 'src/enum';
@ -24,7 +24,8 @@ import { isSmartSearchEnabled } from 'src/utils/misc';
@Injectable() @Injectable()
export class SearchService extends BaseService { export class SearchService extends BaseService {
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> { async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden }); const people = await this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
return people.map((person) => mapPerson(person));
} }
async searchPlaces(dto: SearchPlacesDto): Promise<PlacesResponseDto[]> { async searchPlaces(dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {

View File

@ -12,6 +12,7 @@ import {
IsArray, IsArray,
IsBoolean, IsBoolean,
IsDate, IsDate,
IsHexColor,
IsNotEmpty, IsNotEmpty,
IsOptional, IsOptional,
IsString, IsString,
@ -97,6 +98,15 @@ export function Optional({ nullable, emptyToNull, ...validationOptions }: Option
return applyDecorators(...decorators); return applyDecorators(...decorators);
} }
export const ValidateHexColor = () => {
const decorators = [
IsHexColor(),
Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)),
];
return applyDecorators(...decorators);
};
type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean }; type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean };
export const ValidateUUID = (options?: UUIDOptions) => { export const ValidateUUID = (options?: UUIDOptions) => {
const { optional, each, nullable } = { optional: false, each: false, nullable: false, ...options }; const { optional, each, nullable } = { optional: false, each: false, nullable: false, ...options };