diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index c4d4707f3b..d5251cbc96 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -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} diff --git a/mobile/openapi/doc/PeopleUpdateItem.md b/mobile/openapi/doc/PeopleUpdateItem.md index 43a1b02259..25152c4e4b 100644 Binary files a/mobile/openapi/doc/PeopleUpdateItem.md and b/mobile/openapi/doc/PeopleUpdateItem.md differ diff --git a/mobile/openapi/doc/PersonResponseDto.md b/mobile/openapi/doc/PersonResponseDto.md index e43d67a611..c2acbacd1b 100644 Binary files a/mobile/openapi/doc/PersonResponseDto.md and b/mobile/openapi/doc/PersonResponseDto.md differ diff --git a/mobile/openapi/doc/PersonUpdateDto.md b/mobile/openapi/doc/PersonUpdateDto.md index 935b4348c3..a4df668785 100644 Binary files a/mobile/openapi/doc/PersonUpdateDto.md and b/mobile/openapi/doc/PersonUpdateDto.md differ diff --git a/mobile/openapi/lib/model/people_update_item.dart b/mobile/openapi/lib/model/people_update_item.dart index 3a35c8a58e..0abb7a474c 100644 Binary files a/mobile/openapi/lib/model/people_update_item.dart and b/mobile/openapi/lib/model/people_update_item.dart differ diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index 21120f23b8..5e65d947a4 100644 Binary files a/mobile/openapi/lib/model/person_response_dto.dart and b/mobile/openapi/lib/model/person_response_dto.dart differ diff --git a/mobile/openapi/lib/model/person_update_dto.dart b/mobile/openapi/lib/model/person_update_dto.dart index baa985b1c7..fc384c842e 100644 Binary files a/mobile/openapi/lib/model/person_update_dto.dart and b/mobile/openapi/lib/model/person_update_dto.dart differ diff --git a/mobile/openapi/test/people_update_item_test.dart b/mobile/openapi/test/people_update_item_test.dart index 9c366e4ebe..4c91143bd5 100644 Binary files a/mobile/openapi/test/people_update_item_test.dart and b/mobile/openapi/test/people_update_item_test.dart differ diff --git a/mobile/openapi/test/person_response_dto_test.dart b/mobile/openapi/test/person_response_dto_test.dart index 8b9f7bec8a..0ba7306117 100644 Binary files a/mobile/openapi/test/person_response_dto_test.dart and b/mobile/openapi/test/person_response_dto_test.dart differ diff --git a/mobile/openapi/test/person_update_dto_test.dart b/mobile/openapi/test/person_update_dto_test.dart index b515c2c8e1..80c46e44f2 100644 Binary files a/mobile/openapi/test/person_update_dto_test.dart and b/mobile/openapi/test/person_update_dto_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index ba6c5cb7a9..5e561b9109 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -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" diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index f9557fbaea..71fe0bd41f 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -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, }; diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index e5bca7c830..b75bea23f8 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -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); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 187ef3358d..07a41400b3 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -63,11 +63,13 @@ export class PersonService { async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise { 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) { diff --git a/server/src/infra/entities/person.entity.ts b/server/src/infra/entities/person.entity.ts index b93c4bbf9d..b0da2f63da 100644 --- a/server/src/infra/entities/person.entity.ts +++ b/server/src/infra/entities/person.entity.ts @@ -30,6 +30,9 @@ export class PersonEntity { @Column({ default: '' }) name!: string; + @Column({ type: 'date', nullable: true }) + birthDate!: Date | null; + @Column({ default: '' }) thumbnailPath!: string; diff --git a/server/src/infra/migrations/1692112147855-AddPersonBirthDate.ts b/server/src/infra/migrations/1692112147855-AddPersonBirthDate.ts new file mode 100644 index 0000000000..db2ba35dad --- /dev/null +++ b/server/src/infra/migrations/1692112147855-AddPersonBirthDate.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class AddPersonBirthDate1692112147855 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "person" ADD "birthDate" date`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "birthDate"`); + } + +} diff --git a/server/test/e2e/person.e2e-spec.ts b/server/test/e2e/person.e2e-spec.ts new file mode 100644 index 0000000000..6395e78b0f --- /dev/null +++ b/server/test/e2e/person.e2e-spec.ts @@ -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); + 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); + 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 }); + }); + }); +}); diff --git a/server/test/fixtures/person.stub.ts b/server/test/fixtures/person.stub.ts index f2b512b88f..2d419425d0 100644 --- a/server/test/fixtures/person.stub.ts +++ b/server/test/fixtures/person.stub.ts @@ -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({ + 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({ + 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, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index c4d4707f3b..d5251cbc96 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -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} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 3f574cce7c..2c77cd8af7 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -121,6 +121,13 @@ thumbhash={null} />

{person.name}

+

+ {#if person.birthDate} + Age {Math.floor( + DateTime.fromISO(asset.fileCreatedAt).diff(DateTime.fromISO(person.birthDate), 'years').years, + )} + {/if} +

{/each} diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 9edeb27761..3b1dc02176 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -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; + }>();
@@ -52,9 +45,10 @@ {#if showContextMenu} (showContextMenu = false)}> - onHideFaceClicked()} text="Hide face" /> - onChangeNameClicked()} text="Change name" /> - onMergeFacesClicked()} text="Merge faces" /> + dispatch('hide-face')} text="Hide face" /> + dispatch('change-name')} text="Change name" /> + dispatch('set-birth-date')} text="Set date of birth" /> + dispatch('merge-faces')} text="Merge faces" /> {/if} diff --git a/web/src/lib/components/faces-page/set-birth-date-modal.svelte b/web/src/lib/components/faces-page/set-birth-date-modal.svelte new file mode 100644 index 0000000000..20ce4d3822 --- /dev/null +++ b/web/src/lib/components/faces-page/set-birth-date-modal.svelte @@ -0,0 +1,43 @@ + + + handleCancel()}> +
+
+ +

Set date of birth

+ +

+ Date of birth is used to calculate the age of this person at the time of a photo. +

+
+ +
handleSubmit()} autocomplete="off"> +
+ +
+
+ + +
+
+
+
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 63a59ab98f..4fe4576f1b 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -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) => { + const handleChangeName = (detail: PersonResponseDto) => { showChangeNameModal = true; personName = detail.name; personMerge1 = detail; edittingPerson = detail; }; - const handleHideFace = async (event: CustomEvent) => { + 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) => { - 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} handleChangeName(person)} + on:set-birth-date={() => handleSetBirthDate(person)} + on:merge-faces={() => handleMergeFaces(person)} + on:hide-face={() => handleHideFace(person)} /> {/if} {/each} @@ -372,6 +405,14 @@
{/if} + + {#if showSetBirthDateModal} + (showSetBirthDateModal = false)} + on:updated={(event) => submitBirthDateChange(event.detail)} + /> + {/if} {#if selectHidden} { + 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'); + } + }; {#if viewMode === ViewMode.SUGGEST_MERGE} @@ -185,6 +210,14 @@ /> {/if} +{#if viewMode === ViewMode.BIRTH_DATE} + (viewMode = ViewMode.VIEW_ASSETS)} + on:updated={(event) => handleSetBirthDate(event.detail)} + /> +{/if} + {#if viewMode === ViewMode.MERGE_FACES} (viewMode = ViewMode.VIEW_ASSETS)} /> {/if} @@ -206,11 +239,12 @@ {:else} - {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE} + {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} goto(previousRoute)}> (viewMode = ViewMode.SELECT_FACE)} /> + (viewMode = ViewMode.BIRTH_DATE)} /> (viewMode = ViewMode.MERGE_FACES)} /> @@ -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}
{#if isEditingName}