You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat: set person birth date (web only) (#3721)
* Person birth date (data layer) * Person birth date (data layer) * Person birth date (service layer) * Person birth date (service layer, API) * Person birth date (service layer, API) * Person birth date (UI) (wip) * Person birth date (UI) (wip) * Person birth date (UI) (wip) * Person birth date (UI) (wip) * UI: Use "date of birth" everywhere * UI: better modal dialog Similar to the API key modal. * UI: set date of birth from people page * Use typed events for modal dispatcher * Date of birth tests (wip) * Regenerate API * Code formatting * Fix Svelte typing * Fix Svelte typing * Fix person model [skip ci] * Minor refactoring [skip ci] * Typed event dispatcher [skip ci] * Refactor typed event dispatcher [skip ci] * Fix unchanged birthdate check [skip ci] * Remove unnecessary custom transformer [skip ci] * PersonUpdate: call search index update job only when needed * Regenerate API * Code formatting * Fix tests * Fix DTO * Regenerate API * chore: verbiage and view mode * feat: show current age * test: person e2e * fix: show name for birth date selection --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							
								
								
									
										18
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -1840,6 +1840,12 @@ export interface PeopleUpdateDto { | ||||
|  * @interface PeopleUpdateItem | ||||
|  */ | ||||
| export interface PeopleUpdateItem { | ||||
|     /** | ||||
|      * Person date of birth. | ||||
|      * @type {string} | ||||
|      * @memberof PeopleUpdateItem | ||||
|      */ | ||||
|     'birthDate'?: string | null; | ||||
|     /** | ||||
|      * Asset is used to get the feature face thumbnail. | ||||
|      * @type {string} | ||||
| @@ -1871,6 +1877,12 @@ export interface PeopleUpdateItem { | ||||
|  * @interface PersonResponseDto | ||||
|  */ | ||||
| export interface PersonResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof PersonResponseDto | ||||
|      */ | ||||
|     'birthDate': string | null; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
| @@ -1902,6 +1914,12 @@ export interface PersonResponseDto { | ||||
|  * @interface PersonUpdateDto | ||||
|  */ | ||||
| export interface PersonUpdateDto { | ||||
|     /** | ||||
|      * Person date of birth. | ||||
|      * @type {string} | ||||
|      * @memberof PersonUpdateDto | ||||
|      */ | ||||
|     'birthDate'?: string | null; | ||||
|     /** | ||||
|      * Asset is used to get the feature face thumbnail. | ||||
|      * @type {string} | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/PeopleUpdateItem.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/PeopleUpdateItem.md
									
									
									
										generated
									
									
									
								
							| @@ -8,6 +8,7 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **birthDate** | [**DateTime**](DateTime.md) | Person date of birth. | [optional]  | ||||
| **featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]  | ||||
| **id** | **String** | Person id. |  | ||||
| **isHidden** | **bool** | Person visibility | [optional]  | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/PersonResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/PersonResponseDto.md
									
									
									
										generated
									
									
									
								
							| @@ -8,6 +8,7 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **birthDate** | [**DateTime**](DateTime.md) |  |  | ||||
| **id** | **String** |  |  | ||||
| **isHidden** | **bool** |  |  | ||||
| **name** | **String** |  |  | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/PersonUpdateDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/PersonUpdateDto.md
									
									
									
										generated
									
									
									
								
							| @@ -8,6 +8,7 @@ import 'package:openapi/api.dart'; | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **birthDate** | [**DateTime**](DateTime.md) | Person date of birth. | [optional]  | ||||
| **featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]  | ||||
| **isHidden** | **bool** | Person visibility | [optional]  | ||||
| **name** | **String** | Person name. | [optional]  | ||||
|   | ||||
							
								
								
									
										14
									
								
								mobile/openapi/lib/model/people_update_item.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/lib/model/people_update_item.dart
									
									
									
										generated
									
									
									
								
							| @@ -13,12 +13,16 @@ part of openapi.api; | ||||
| class PeopleUpdateItem { | ||||
|   /// Returns a new [PeopleUpdateItem] instance. | ||||
|   PeopleUpdateItem({ | ||||
|     this.birthDate, | ||||
|     this.featureFaceAssetId, | ||||
|     required this.id, | ||||
|     this.isHidden, | ||||
|     this.name, | ||||
|   }); | ||||
| 
 | ||||
|   /// Person date of birth. | ||||
|   DateTime? birthDate; | ||||
| 
 | ||||
|   /// Asset is used to get the feature face thumbnail. | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
| @@ -51,6 +55,7 @@ class PeopleUpdateItem { | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateItem && | ||||
|      other.birthDate == birthDate && | ||||
|      other.featureFaceAssetId == featureFaceAssetId && | ||||
|      other.id == id && | ||||
|      other.isHidden == isHidden && | ||||
| @@ -59,16 +64,22 @@ class PeopleUpdateItem { | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (birthDate == null ? 0 : birthDate!.hashCode) + | ||||
|     (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) + | ||||
|     (id.hashCode) + | ||||
|     (isHidden == null ? 0 : isHidden!.hashCode) + | ||||
|     (name == null ? 0 : name!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'PeopleUpdateItem[featureFaceAssetId=$featureFaceAssetId, id=$id, isHidden=$isHidden, name=$name]'; | ||||
|   String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isHidden=$isHidden, name=$name]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|     if (this.birthDate != null) { | ||||
|       json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); | ||||
|     } else { | ||||
|     //  json[r'birthDate'] = null; | ||||
|     } | ||||
|     if (this.featureFaceAssetId != null) { | ||||
|       json[r'featureFaceAssetId'] = this.featureFaceAssetId; | ||||
|     } else { | ||||
| @@ -96,6 +107,7 @@ class PeopleUpdateItem { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return PeopleUpdateItem( | ||||
|         birthDate: mapDateTime(json, r'birthDate', ''), | ||||
|         featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'), | ||||
|         id: mapValueOfType<String>(json, r'id')!, | ||||
|         isHidden: mapValueOfType<bool>(json, r'isHidden'), | ||||
|   | ||||
							
								
								
									
										14
									
								
								mobile/openapi/lib/model/person_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/lib/model/person_response_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -13,12 +13,15 @@ part of openapi.api; | ||||
| class PersonResponseDto { | ||||
|   /// Returns a new [PersonResponseDto] instance. | ||||
|   PersonResponseDto({ | ||||
|     required this.birthDate, | ||||
|     required this.id, | ||||
|     required this.isHidden, | ||||
|     required this.name, | ||||
|     required this.thumbnailPath, | ||||
|   }); | ||||
| 
 | ||||
|   DateTime? birthDate; | ||||
| 
 | ||||
|   String id; | ||||
| 
 | ||||
|   bool isHidden; | ||||
| @@ -29,6 +32,7 @@ class PersonResponseDto { | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto && | ||||
|      other.birthDate == birthDate && | ||||
|      other.id == id && | ||||
|      other.isHidden == isHidden && | ||||
|      other.name == name && | ||||
| @@ -37,16 +41,22 @@ class PersonResponseDto { | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (birthDate == null ? 0 : birthDate!.hashCode) + | ||||
|     (id.hashCode) + | ||||
|     (isHidden.hashCode) + | ||||
|     (name.hashCode) + | ||||
|     (thumbnailPath.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'PersonResponseDto[id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]'; | ||||
|   String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|     if (this.birthDate != null) { | ||||
|       json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); | ||||
|     } else { | ||||
|     //  json[r'birthDate'] = null; | ||||
|     } | ||||
|       json[r'id'] = this.id; | ||||
|       json[r'isHidden'] = this.isHidden; | ||||
|       json[r'name'] = this.name; | ||||
| @@ -62,6 +72,7 @@ class PersonResponseDto { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return PersonResponseDto( | ||||
|         birthDate: mapDateTime(json, r'birthDate', ''), | ||||
|         id: mapValueOfType<String>(json, r'id')!, | ||||
|         isHidden: mapValueOfType<bool>(json, r'isHidden')!, | ||||
|         name: mapValueOfType<String>(json, r'name')!, | ||||
| @@ -113,6 +124,7 @@ class PersonResponseDto { | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'birthDate', | ||||
|     'id', | ||||
|     'isHidden', | ||||
|     'name', | ||||
|   | ||||
							
								
								
									
										14
									
								
								mobile/openapi/lib/model/person_update_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/lib/model/person_update_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -13,11 +13,15 @@ part of openapi.api; | ||||
| class PersonUpdateDto { | ||||
|   /// Returns a new [PersonUpdateDto] instance. | ||||
|   PersonUpdateDto({ | ||||
|     this.birthDate, | ||||
|     this.featureFaceAssetId, | ||||
|     this.isHidden, | ||||
|     this.name, | ||||
|   }); | ||||
| 
 | ||||
|   /// Person date of birth. | ||||
|   DateTime? birthDate; | ||||
| 
 | ||||
|   /// Asset is used to get the feature face thumbnail. | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
| @@ -47,6 +51,7 @@ class PersonUpdateDto { | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto && | ||||
|      other.birthDate == birthDate && | ||||
|      other.featureFaceAssetId == featureFaceAssetId && | ||||
|      other.isHidden == isHidden && | ||||
|      other.name == name; | ||||
| @@ -54,15 +59,21 @@ class PersonUpdateDto { | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (birthDate == null ? 0 : birthDate!.hashCode) + | ||||
|     (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) + | ||||
|     (isHidden == null ? 0 : isHidden!.hashCode) + | ||||
|     (name == null ? 0 : name!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'PersonUpdateDto[featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden, name=$name]'; | ||||
|   String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden, name=$name]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|     if (this.birthDate != null) { | ||||
|       json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); | ||||
|     } else { | ||||
|     //  json[r'birthDate'] = null; | ||||
|     } | ||||
|     if (this.featureFaceAssetId != null) { | ||||
|       json[r'featureFaceAssetId'] = this.featureFaceAssetId; | ||||
|     } else { | ||||
| @@ -89,6 +100,7 @@ class PersonUpdateDto { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return PersonUpdateDto( | ||||
|         birthDate: mapDateTime(json, r'birthDate', ''), | ||||
|         featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'), | ||||
|         isHidden: mapValueOfType<bool>(json, r'isHidden'), | ||||
|         name: mapValueOfType<String>(json, r'name'), | ||||
|   | ||||
							
								
								
									
										6
									
								
								mobile/openapi/test/people_update_item_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/test/people_update_item_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,6 +16,12 @@ void main() { | ||||
|   // final instance = PeopleUpdateItem(); | ||||
| 
 | ||||
|   group('test PeopleUpdateItem', () { | ||||
|     // Person date of birth. | ||||
|     // DateTime birthDate | ||||
|     test('to test the property `birthDate`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // Asset is used to get the feature face thumbnail. | ||||
|     // String featureFaceAssetId | ||||
|     test('to test the property `featureFaceAssetId`', () async { | ||||
|   | ||||
| @@ -16,6 +16,11 @@ void main() { | ||||
|   // final instance = PersonResponseDto(); | ||||
| 
 | ||||
|   group('test PersonResponseDto', () { | ||||
|     // DateTime birthDate | ||||
|     test('to test the property `birthDate`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // String id | ||||
|     test('to test the property `id`', () async { | ||||
|       // TODO | ||||
|   | ||||
							
								
								
									
										6
									
								
								mobile/openapi/test/person_update_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/test/person_update_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,6 +16,12 @@ void main() { | ||||
|   // final instance = PersonUpdateDto(); | ||||
| 
 | ||||
|   group('test PersonUpdateDto', () { | ||||
|     // Person date of birth. | ||||
|     // DateTime birthDate | ||||
|     test('to test the property `birthDate`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // Asset is used to get the feature face thumbnail. | ||||
|     // String featureFaceAssetId | ||||
|     test('to test the property `featureFaceAssetId`', () async { | ||||
|   | ||||
| @@ -6176,6 +6176,12 @@ | ||||
|       }, | ||||
|       "PeopleUpdateItem": { | ||||
|         "properties": { | ||||
|           "birthDate": { | ||||
|             "description": "Person date of birth.", | ||||
|             "format": "date", | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "featureFaceAssetId": { | ||||
|             "description": "Asset is used to get the feature face thumbnail.", | ||||
|             "type": "string" | ||||
| @@ -6200,6 +6206,11 @@ | ||||
|       }, | ||||
|       "PersonResponseDto": { | ||||
|         "properties": { | ||||
|           "birthDate": { | ||||
|             "format": "date", | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "id": { | ||||
|             "type": "string" | ||||
|           }, | ||||
| @@ -6214,6 +6225,7 @@ | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "birthDate", | ||||
|           "id", | ||||
|           "name", | ||||
|           "thumbnailPath", | ||||
| @@ -6223,6 +6235,12 @@ | ||||
|       }, | ||||
|       "PersonUpdateDto": { | ||||
|         "properties": { | ||||
|           "birthDate": { | ||||
|             "description": "Person date of birth.", | ||||
|             "format": "date", | ||||
|             "nullable": true, | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "featureFaceAssetId": { | ||||
|             "description": "Asset is used to get the feature face thumbnail.", | ||||
|             "type": "string" | ||||
|   | ||||
| @@ -1,7 +1,16 @@ | ||||
| import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; | ||||
| import { ApiProperty } from '@nestjs/swagger'; | ||||
| import { Transform, Type } from 'class-transformer'; | ||||
| import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; | ||||
| import { | ||||
|   IsArray, | ||||
|   IsBoolean, | ||||
|   IsDate, | ||||
|   IsNotEmpty, | ||||
|   IsOptional, | ||||
|   IsString, | ||||
|   ValidateIf, | ||||
|   ValidateNested, | ||||
| } from 'class-validator'; | ||||
| import { toBoolean, ValidateUUID } from '../domain.util'; | ||||
|  | ||||
| export class PersonUpdateDto { | ||||
| @@ -12,6 +21,16 @@ export class PersonUpdateDto { | ||||
|   @IsString() | ||||
|   name?: string; | ||||
|  | ||||
|   /** | ||||
|    * Person date of birth. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @IsDate() | ||||
|   @Type(() => Date) | ||||
|   @ValidateIf((value) => value !== null) | ||||
|   @ApiProperty({ format: 'date' }) | ||||
|   birthDate?: Date | null; | ||||
|  | ||||
|   /** | ||||
|    * Asset is used to get the feature face thumbnail. | ||||
|    */ | ||||
| @@ -49,6 +68,15 @@ export class PeopleUpdateItem { | ||||
|   @IsString() | ||||
|   name?: string; | ||||
|  | ||||
|   /** | ||||
|    * Person date of birth. | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @IsDate() | ||||
|   @Type(() => Date) | ||||
|   @ApiProperty({ format: 'date' }) | ||||
|   birthDate?: Date | null; | ||||
|  | ||||
|   /** | ||||
|    * Asset is used to get the feature face thumbnail. | ||||
|    */ | ||||
| @@ -78,6 +106,8 @@ export class PersonSearchDto { | ||||
| export class PersonResponseDto { | ||||
|   id!: string; | ||||
|   name!: string; | ||||
|   @ApiProperty({ format: 'date' }) | ||||
|   birthDate!: Date | null; | ||||
|   thumbnailPath!: string; | ||||
|   isHidden!: boolean; | ||||
| } | ||||
| @@ -96,6 +126,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto { | ||||
|   return { | ||||
|     id: person.id, | ||||
|     name: person.name, | ||||
|     birthDate: person.birthDate, | ||||
|     thumbnailPath: person.thumbnailPath, | ||||
|     isHidden: person.isHidden, | ||||
|   }; | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import { PersonService } from './person.service'; | ||||
| const responseDto: PersonResponseDto = { | ||||
|   id: 'person-1', | ||||
|   name: 'Person 1', | ||||
|   birthDate: null, | ||||
|   thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|   isHidden: false, | ||||
| }; | ||||
| @@ -68,6 +69,7 @@ describe(PersonService.name, () => { | ||||
|           { | ||||
|             id: 'person-1', | ||||
|             name: '', | ||||
|             birthDate: null, | ||||
|             thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|             isHidden: true, | ||||
|           }, | ||||
| @@ -142,6 +144,24 @@ describe(PersonService.name, () => { | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it("should update a person's date of birth", async () => { | ||||
|       personMock.getById.mockResolvedValue(personStub.noBirthDate); | ||||
|       personMock.update.mockResolvedValue(personStub.withBirthDate); | ||||
|       personMock.getAssets.mockResolvedValue([assetStub.image]); | ||||
|  | ||||
|       await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({ | ||||
|         id: 'person-1', | ||||
|         name: 'Person 1', | ||||
|         birthDate: new Date('1976-06-30'), | ||||
|         thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|         isHidden: false, | ||||
|       }); | ||||
|  | ||||
|       expect(personMock.getById).toHaveBeenCalledWith('admin_id', 'person-1'); | ||||
|       expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); | ||||
|       expect(jobMock.queue).not.toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it('should update a person visibility', async () => { | ||||
|       personMock.getById.mockResolvedValue(personStub.hidden); | ||||
|       personMock.update.mockResolvedValue(personStub.withName); | ||||
|   | ||||
| @@ -63,11 +63,13 @@ export class PersonService { | ||||
|   async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> { | ||||
|     let person = await this.findOrFail(authUser, id); | ||||
|  | ||||
|     if (dto.name != undefined || dto.isHidden !== undefined) { | ||||
|       person = await this.repository.update({ id, name: dto.name, isHidden: dto.isHidden }); | ||||
|       const assets = await this.repository.getAssets(authUser.id, id); | ||||
|       const ids = assets.map((asset) => asset.id); | ||||
|       await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } }); | ||||
|     if (dto.name !== undefined || dto.birthDate !== undefined || dto.isHidden !== undefined) { | ||||
|       person = await this.repository.update({ id, name: dto.name, birthDate: dto.birthDate, isHidden: dto.isHidden }); | ||||
|       if (this.needsSearchIndexUpdate(dto)) { | ||||
|         const assets = await this.repository.getAssets(authUser.id, id); | ||||
|         const ids = assets.map((asset) => asset.id); | ||||
|         await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } }); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (dto.featureFaceAssetId) { | ||||
| @@ -104,6 +106,7 @@ export class PersonService { | ||||
|         await this.update(authUser, person.id, { | ||||
|           isHidden: person.isHidden, | ||||
|           name: person.name, | ||||
|           birthDate: person.birthDate, | ||||
|           featureFaceAssetId: person.featureFaceAssetId, | ||||
|         }), | ||||
|           results.push({ id: person.id, success: true }); | ||||
| @@ -170,6 +173,15 @@ export class PersonService { | ||||
|     return results; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Returns true if the given person update is going to require an update of the search index. | ||||
|    * @param dto the Person going to be updated | ||||
|    * @private | ||||
|    */ | ||||
|   private needsSearchIndexUpdate(dto: PersonUpdateDto): boolean { | ||||
|     return dto.name !== undefined || dto.isHidden !== undefined; | ||||
|   } | ||||
|  | ||||
|   private async findOrFail(authUser: AuthUserDto, id: string) { | ||||
|     const person = await this.repository.getById(authUser.id, id); | ||||
|     if (!person) { | ||||
|   | ||||
| @@ -30,6 +30,9 @@ export class PersonEntity { | ||||
|   @Column({ default: '' }) | ||||
|   name!: string; | ||||
|  | ||||
|   @Column({ type: 'date', nullable: true }) | ||||
|   birthDate!: Date | null; | ||||
|  | ||||
|   @Column({ default: '' }) | ||||
|   thumbnailPath!: string; | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,13 @@ | ||||
| import { MigrationInterface, QueryRunner } from "typeorm" | ||||
|  | ||||
| export class AddPersonBirthDate1692112147855 implements MigrationInterface { | ||||
|  | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|       await queryRunner.query(`ALTER TABLE "person" ADD "birthDate" date`); | ||||
|     } | ||||
|  | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|       await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "birthDate"`); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										81
									
								
								server/test/e2e/person.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								server/test/e2e/person.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| import { IPersonRepository, LoginResponseDto } from '@app/domain'; | ||||
| import { AppModule, PersonController } from '@app/immich'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import request from 'supertest'; | ||||
| import { errorStub, uuidStub } from '../fixtures'; | ||||
| import { api, db } from '../test-utils'; | ||||
|  | ||||
| describe(`${PersonController.name}`, () => { | ||||
|   let app: INestApplication; | ||||
|   let server: any; | ||||
|   let loginResponse: LoginResponseDto; | ||||
|   let accessToken: string; | ||||
|  | ||||
|   beforeAll(async () => { | ||||
|     const moduleFixture: TestingModule = await Test.createTestingModule({ | ||||
|       imports: [AppModule], | ||||
|     }).compile(); | ||||
|  | ||||
|     app = await moduleFixture.createNestApplication().init(); | ||||
|     server = app.getHttpServer(); | ||||
|   }); | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     await db.reset(); | ||||
|     await api.adminSignUp(server); | ||||
|     loginResponse = await api.adminLogin(server); | ||||
|     accessToken = loginResponse.accessToken; | ||||
|   }); | ||||
|  | ||||
|   afterAll(async () => { | ||||
|     await db.disconnect(); | ||||
|     await app.close(); | ||||
|   }); | ||||
|  | ||||
|   describe('PUT /person/:id', () => { | ||||
|     it('should require authentication', async () => { | ||||
|       const { status, body } = await request(server).put(`/person/${uuidStub.notFound}`); | ||||
|       expect(status).toBe(401); | ||||
|       expect(body).toEqual(errorStub.unauthorized); | ||||
|     }); | ||||
|  | ||||
|     it('should not accept invalid dates', async () => { | ||||
|       for (const birthDate of [false, 'false', '123567', 123456]) { | ||||
|         const { status, body } = await request(server) | ||||
|           .put(`/person/${uuidStub.notFound}`) | ||||
|           .set('Authorization', `Bearer ${accessToken}`) | ||||
|           .send({ birthDate }); | ||||
|         expect(status).toBe(400); | ||||
|         expect(body).toEqual(errorStub.badRequest); | ||||
|       } | ||||
|     }); | ||||
|     it('should update a date of birth', async () => { | ||||
|       const personRepository = app.get<IPersonRepository>(IPersonRepository); | ||||
|       const person = await personRepository.create({ ownerId: loginResponse.userId }); | ||||
|       const { status, body } = await request(server) | ||||
|         .put(`/person/${person.id}`) | ||||
|         .set('Authorization', `Bearer ${accessToken}`) | ||||
|         .send({ birthDate: '1990-01-01T05:00:00.000Z' }); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toMatchObject({ birthDate: '1990-01-01' }); | ||||
|     }); | ||||
|  | ||||
|     it('should clear a date of birth', async () => { | ||||
|       const personRepository = app.get<IPersonRepository>(IPersonRepository); | ||||
|       const person = await personRepository.create({ | ||||
|         birthDate: new Date('1990-01-01'), | ||||
|         ownerId: loginResponse.userId, | ||||
|       }); | ||||
|  | ||||
|       expect(person.birthDate).toBeDefined(); | ||||
|  | ||||
|       const { status, body } = await request(server) | ||||
|         .put(`/person/${person.id}`) | ||||
|         .set('Authorization', `Bearer ${accessToken}`) | ||||
|         .send({ birthDate: null }); | ||||
|       expect(status).toBe(200); | ||||
|       expect(body).toMatchObject({ birthDate: null }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										31
									
								
								server/test/fixtures/person.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								server/test/fixtures/person.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -9,6 +9,7 @@ export const personStub = { | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: '', | ||||
|     birthDate: null, | ||||
|     thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
| @@ -20,6 +21,7 @@ export const personStub = { | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: '', | ||||
|     birthDate: null, | ||||
|     thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: true, | ||||
| @@ -31,6 +33,31 @@ export const personStub = { | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: 'Person 1', | ||||
|     birthDate: null, | ||||
|     thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   }), | ||||
|   noBirthDate: Object.freeze<PersonEntity>({ | ||||
|     id: 'person-1', | ||||
|     createdAt: new Date('2021-01-01'), | ||||
|     updatedAt: new Date('2021-01-01'), | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: 'Person 1', | ||||
|     birthDate: null, | ||||
|     thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   }), | ||||
|   withBirthDate: Object.freeze<PersonEntity>({ | ||||
|     id: 'person-1', | ||||
|     createdAt: new Date('2021-01-01'), | ||||
|     updatedAt: new Date('2021-01-01'), | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: 'Person 1', | ||||
|     birthDate: new Date('1976-06-30'), | ||||
|     thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
| @@ -42,6 +69,7 @@ export const personStub = { | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: '', | ||||
|     birthDate: null, | ||||
|     thumbnailPath: '', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
| @@ -53,6 +81,7 @@ export const personStub = { | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: '', | ||||
|     birthDate: null, | ||||
|     thumbnailPath: '/new/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
| @@ -64,6 +93,7 @@ export const personStub = { | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: 'Person 1', | ||||
|     birthDate: null, | ||||
|     thumbnailPath: '/path/to/thumbnail', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
| @@ -75,6 +105,7 @@ export const personStub = { | ||||
|     ownerId: userStub.admin.id, | ||||
|     owner: userStub.admin, | ||||
|     name: 'Person 2', | ||||
|     birthDate: null, | ||||
|     thumbnailPath: '/path/to/thumbnail', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   | ||||
							
								
								
									
										18
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -1840,6 +1840,12 @@ export interface PeopleUpdateDto { | ||||
|  * @interface PeopleUpdateItem | ||||
|  */ | ||||
| export interface PeopleUpdateItem { | ||||
|     /** | ||||
|      * Person date of birth. | ||||
|      * @type {string} | ||||
|      * @memberof PeopleUpdateItem | ||||
|      */ | ||||
|     'birthDate'?: string | null; | ||||
|     /** | ||||
|      * Asset is used to get the feature face thumbnail. | ||||
|      * @type {string} | ||||
| @@ -1871,6 +1877,12 @@ export interface PeopleUpdateItem { | ||||
|  * @interface PersonResponseDto | ||||
|  */ | ||||
| export interface PersonResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof PersonResponseDto | ||||
|      */ | ||||
|     'birthDate': string | null; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
| @@ -1902,6 +1914,12 @@ export interface PersonResponseDto { | ||||
|  * @interface PersonUpdateDto | ||||
|  */ | ||||
| export interface PersonUpdateDto { | ||||
|     /** | ||||
|      * Person date of birth. | ||||
|      * @type {string} | ||||
|      * @memberof PersonUpdateDto | ||||
|      */ | ||||
|     'birthDate'?: string | null; | ||||
|     /** | ||||
|      * Asset is used to get the feature face thumbnail. | ||||
|      * @type {string} | ||||
|   | ||||
| @@ -121,6 +121,13 @@ | ||||
|               thumbhash={null} | ||||
|             /> | ||||
|             <p class="mt-1 truncate font-medium">{person.name}</p> | ||||
|             <p class="font-light"> | ||||
|               {#if person.birthDate} | ||||
|                 Age {Math.floor( | ||||
|                   DateTime.fromISO(asset.fileCreatedAt).diff(DateTime.fromISO(person.birthDate), 'years').years, | ||||
|                 )} | ||||
|               {/if} | ||||
|             </p> | ||||
|           </a> | ||||
|         {/each} | ||||
|       </div> | ||||
|   | ||||
| @@ -11,19 +11,12 @@ | ||||
|   export let person: PersonResponseDto; | ||||
|  | ||||
|   let showContextMenu = false; | ||||
|   let dispatch = createEventDispatcher(); | ||||
|  | ||||
|   const onChangeNameClicked = () => { | ||||
|     dispatch('change-name', person); | ||||
|   }; | ||||
|  | ||||
|   const onMergeFacesClicked = () => { | ||||
|     dispatch('merge-faces', person); | ||||
|   }; | ||||
|  | ||||
|   const onHideFaceClicked = () => { | ||||
|     dispatch('hide-face', person); | ||||
|   }; | ||||
|   let dispatch = createEventDispatcher<{ | ||||
|     'change-name': void; | ||||
|     'set-birth-date': void; | ||||
|     'merge-faces': void; | ||||
|     'hide-face': void; | ||||
|   }>(); | ||||
| </script> | ||||
|  | ||||
| <div id="people-card" class="relative"> | ||||
| @@ -52,9 +45,10 @@ | ||||
|  | ||||
|     {#if showContextMenu} | ||||
|       <ContextMenu on:outclick={() => (showContextMenu = false)}> | ||||
|         <MenuOption on:click={() => onHideFaceClicked()} text="Hide face" /> | ||||
|         <MenuOption on:click={() => onChangeNameClicked()} text="Change name" /> | ||||
|         <MenuOption on:click={() => onMergeFacesClicked()} text="Merge faces" /> | ||||
|         <MenuOption on:click={() => dispatch('hide-face')} text="Hide face" /> | ||||
|         <MenuOption on:click={() => dispatch('change-name')} text="Change name" /> | ||||
|         <MenuOption on:click={() => dispatch('set-birth-date')} text="Set date of birth" /> | ||||
|         <MenuOption on:click={() => dispatch('merge-faces')} text="Merge faces" /> | ||||
|       </ContextMenu> | ||||
|     {/if} | ||||
|   </button> | ||||
|   | ||||
| @@ -0,0 +1,43 @@ | ||||
| <script lang="ts"> | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|   import Cake from 'svelte-material-icons/Cake.svelte'; | ||||
|   import Button from '../elements/buttons/button.svelte'; | ||||
|   import FullScreenModal from '../shared-components/full-screen-modal.svelte'; | ||||
|  | ||||
|   export let birthDate: string; | ||||
|  | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     close: void; | ||||
|     updated: string; | ||||
|   }>(); | ||||
|  | ||||
|   const handleCancel = () => dispatch('close'); | ||||
|   const handleSubmit = () => dispatch('updated', birthDate); | ||||
| </script> | ||||
|  | ||||
| <FullScreenModal on:clickOutside={() => handleCancel()}> | ||||
|   <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" | ||||
|     > | ||||
|       <Cake size="4em" /> | ||||
|       <h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Set date of birth</h1> | ||||
|  | ||||
|       <p class="text-sm dark:text-immich-dark-fg"> | ||||
|         Date of birth is used to calculate the age of this person at the time of a photo. | ||||
|       </p> | ||||
|     </div> | ||||
|  | ||||
|     <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off"> | ||||
|       <div class="m-4 flex flex-col gap-2"> | ||||
|         <input class="immich-form-input" id="birthDate" name="birthDate" type="date" bind:value={birthDate} /> | ||||
|       </div> | ||||
|       <div class="mt-8 flex w-full gap-4 px-4"> | ||||
|         <Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button> | ||||
|         <Button type="submit" fullwidth>Set</Button> | ||||
|       </div> | ||||
|     </form> | ||||
|   </div> | ||||
| </FullScreenModal> | ||||
| @@ -20,6 +20,7 @@ | ||||
|   import { onDestroy, onMount } from 'svelte'; | ||||
|   import { browser } from '$app/environment'; | ||||
|   import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; | ||||
|   import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|   let selectHidden = false; | ||||
| @@ -35,6 +36,7 @@ | ||||
|   let toggleVisibility = false; | ||||
|  | ||||
|   let showChangeNameModal = false; | ||||
|   let showSetBirthDateModal = false; | ||||
|   let showMergeModal = false; | ||||
|   let personName = ''; | ||||
|   let personMerge1: PersonResponseDto; | ||||
| @@ -194,17 +196,22 @@ | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleChangeName = ({ detail }: CustomEvent<PersonResponseDto>) => { | ||||
|   const handleChangeName = (detail: PersonResponseDto) => { | ||||
|     showChangeNameModal = true; | ||||
|     personName = detail.name; | ||||
|     personMerge1 = detail; | ||||
|     edittingPerson = detail; | ||||
|   }; | ||||
|  | ||||
|   const handleHideFace = async (event: CustomEvent<PersonResponseDto>) => { | ||||
|   const handleSetBirthDate = (detail: PersonResponseDto) => { | ||||
|     showSetBirthDateModal = true; | ||||
|     edittingPerson = detail; | ||||
|   }; | ||||
|  | ||||
|   const handleHideFace = async (detail: PersonResponseDto) => { | ||||
|     try { | ||||
|       const { data: updatedPerson } = await api.personApi.updatePerson({ | ||||
|         id: event.detail.id, | ||||
|         id: detail.id, | ||||
|         personUpdateDto: { isHidden: true }, | ||||
|       }); | ||||
|  | ||||
| @@ -232,16 +239,13 @@ | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleMergeFaces = (event: CustomEvent<PersonResponseDto>) => { | ||||
|     goto(`${AppRoute.PEOPLE}/${event.detail.id}?action=merge`); | ||||
|   const handleMergeFaces = (detail: PersonResponseDto) => { | ||||
|     goto(`${AppRoute.PEOPLE}/${detail.id}?action=merge`); | ||||
|   }; | ||||
|  | ||||
|   const submitNameChange = async () => { | ||||
|     showChangeNameModal = false; | ||||
|     if (!edittingPerson) { | ||||
|       return; | ||||
|     } | ||||
|     if (personName === edittingPerson.name) { | ||||
|     if (!edittingPerson || personName === edittingPerson.name) { | ||||
|       return; | ||||
|     } | ||||
|     // We check if another person has the same name as the name entered by the user | ||||
| @@ -261,6 +265,34 @@ | ||||
|     changeName(); | ||||
|   }; | ||||
|  | ||||
|   const submitBirthDateChange = async (value: string) => { | ||||
|     showSetBirthDateModal = false; | ||||
|     if (!edittingPerson || value === edittingPerson.birthDate) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const { data: updatedPerson } = await api.personApi.updatePerson({ | ||||
|         id: edittingPerson.id, | ||||
|         personUpdateDto: { birthDate: value.length > 0 ? value : null }, | ||||
|       }); | ||||
|  | ||||
|       people = people.map((person: PersonResponseDto) => { | ||||
|         if (person.id === updatedPerson.id) { | ||||
|           return updatedPerson; | ||||
|         } | ||||
|         return person; | ||||
|       }); | ||||
|  | ||||
|       notificationController.show({ | ||||
|         message: 'Date of birth saved succesfully', | ||||
|         type: NotificationType.Info, | ||||
|       }); | ||||
|     } catch (error) { | ||||
|       handleError(error, 'Unable to save name'); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const changeName = async () => { | ||||
|     showMergeModal = false; | ||||
|     showChangeNameModal = false; | ||||
| @@ -323,9 +355,10 @@ | ||||
|           {#if !person.isHidden} | ||||
|             <PeopleCard | ||||
|               {person} | ||||
|               on:change-name={handleChangeName} | ||||
|               on:merge-faces={handleMergeFaces} | ||||
|               on:hide-face={handleHideFace} | ||||
|               on:change-name={() => handleChangeName(person)} | ||||
|               on:set-birth-date={() => handleSetBirthDate(person)} | ||||
|               on:merge-faces={() => handleMergeFaces(person)} | ||||
|               on:hide-face={() => handleHideFace(person)} | ||||
|             /> | ||||
|           {/if} | ||||
|         {/each} | ||||
| @@ -372,6 +405,14 @@ | ||||
|       </div> | ||||
|     </FullScreenModal> | ||||
|   {/if} | ||||
|  | ||||
|   {#if showSetBirthDateModal} | ||||
|     <SetBirthDateModal | ||||
|       birthDate={edittingPerson?.birthDate ?? ''} | ||||
|       on:close={() => (showSetBirthDateModal = false)} | ||||
|       on:updated={(event) => submitBirthDateChange(event.detail)} | ||||
|     /> | ||||
|   {/if} | ||||
| </UserPageLayout> | ||||
| {#if selectHidden} | ||||
|   <ShowHide | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|   import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; | ||||
|   import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; | ||||
|   import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; | ||||
|   import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte'; | ||||
|   import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; | ||||
|   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; | ||||
|   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; | ||||
| @@ -39,6 +40,7 @@ | ||||
|     SELECT_FACE = 'select-face', | ||||
|     MERGE_FACES = 'merge-faces', | ||||
|     SUGGEST_MERGE = 'suggest-merge', | ||||
|     BIRTH_DATE = 'birth-date', | ||||
|   } | ||||
|  | ||||
|   const assetStore = new AssetStore({ | ||||
| @@ -172,6 +174,29 @@ | ||||
|     } | ||||
|     changeName(); | ||||
|   }; | ||||
|  | ||||
|   const handleSetBirthDate = async (birthDate: string) => { | ||||
|     try { | ||||
|       viewMode = ViewMode.VIEW_ASSETS; | ||||
|       data.person.birthDate = birthDate; | ||||
|  | ||||
|       const { data: updatedPerson } = await api.personApi.updatePerson({ | ||||
|         id: data.person.id, | ||||
|         personUpdateDto: { birthDate: birthDate.length > 0 ? birthDate : null }, | ||||
|       }); | ||||
|  | ||||
|       people = people.map((person: PersonResponseDto) => { | ||||
|         if (person.id === updatedPerson.id) { | ||||
|           return updatedPerson; | ||||
|         } | ||||
|         return person; | ||||
|       }); | ||||
|  | ||||
|       notificationController.show({ message: 'Date of birth saved successfully', type: NotificationType.Info }); | ||||
|     } catch (error) { | ||||
|       handleError(error, 'Unable to save date of birth'); | ||||
|     } | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| {#if viewMode === ViewMode.SUGGEST_MERGE} | ||||
| @@ -185,6 +210,14 @@ | ||||
|   /> | ||||
| {/if} | ||||
|  | ||||
| {#if viewMode === ViewMode.BIRTH_DATE} | ||||
|   <SetBirthDateModal | ||||
|     birthDate={data.person.birthDate ?? ''} | ||||
|     on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} | ||||
|     on:updated={(event) => handleSetBirthDate(event.detail)} | ||||
|   /> | ||||
| {/if} | ||||
|  | ||||
| {#if viewMode === ViewMode.MERGE_FACES} | ||||
|   <MergeFaceSelector person={data.person} on:go-back={() => (viewMode = ViewMode.VIEW_ASSETS)} /> | ||||
| {/if} | ||||
| @@ -206,11 +239,12 @@ | ||||
|       </AssetSelectContextMenu> | ||||
|     </AssetSelectControlBar> | ||||
|   {:else} | ||||
|     {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE} | ||||
|     {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} | ||||
|       <ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(previousRoute)}> | ||||
|         <svelte:fragment slot="trailing"> | ||||
|           <AssetSelectContextMenu icon={DotsVertical} title="Menu"> | ||||
|             <MenuOption text="Change feature photo" on:click={() => (viewMode = ViewMode.SELECT_FACE)} /> | ||||
|             <MenuOption text="Set date of birth" on:click={() => (viewMode = ViewMode.BIRTH_DATE)} /> | ||||
|             <MenuOption text="Merge face" on:click={() => (viewMode = ViewMode.MERGE_FACES)} /> | ||||
|           </AssetSelectContextMenu> | ||||
|         </svelte:fragment> | ||||
| @@ -233,7 +267,7 @@ | ||||
|     singleSelect={viewMode === ViewMode.SELECT_FACE} | ||||
|     on:select={({ detail: asset }) => handleSelectFeaturePhoto(asset)} | ||||
|   > | ||||
|     {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE} | ||||
|     {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} | ||||
|       <!-- Face information block --> | ||||
|       <section class="flex place-items-center p-4 sm:px-6"> | ||||
|         {#if isEditingName} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user