feat(web,server): user memory settings (#3628)

* feat(web,server): user preference for time-based memories

* chore: open api

* dev: mobile

* fix: update

* mobile work

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Alex Tran <Alex.Tran@conductix.com>
This commit is contained in:
Jason Rasmussen
2023-08-09 22:01:16 -04:00
committed by GitHub
parent 343087e2b4
commit a6eb227330
33 changed files with 519 additions and 25 deletions

View File

@@ -5396,6 +5396,9 @@
"lastName": {
"type": "string"
},
"memoriesEnabled": {
"type": "boolean"
},
"password": {
"type": "string"
},
@@ -7004,6 +7007,9 @@
"lastName": {
"type": "string"
},
"memoriesEnabled": {
"type": "boolean"
},
"password": {
"type": "string"
},
@@ -7092,6 +7098,9 @@
"lastName": {
"type": "string"
},
"memoriesEnabled": {
"type": "boolean"
},
"oauthId": {
"type": "string"
},
@@ -7123,7 +7132,8 @@
"createdAt",
"deletedAt",
"updatedAt",
"oauthId"
"oauthId",
"memoriesEnabled"
],
"type": "object"
},

View File

@@ -176,6 +176,7 @@ describe(AlbumService.name, () => {
deletedAt: null,
updatedAt: new Date('2021-01-01'),
externalPath: null,
memoriesEnabled: true,
},
ownerId: 'admin_id',
shared: false,

View File

@@ -1,10 +1,11 @@
import { BadRequestException } from '@nestjs/common';
import { authStub, newPartnerRepositoryMock, partnerStub } from '@test';
import { UserResponseDto } from '../index';
import { IPartnerRepository, PartnerDirection } from './partner.repository';
import { PartnerService } from './partner.service';
const responseDto = {
admin: {
admin: <UserResponseDto>{
email: 'admin@test.com',
firstName: 'admin_first_name',
id: 'admin_id',
@@ -18,8 +19,9 @@ const responseDto = {
deletedAt: null,
updatedAt: new Date('2021-01-01'),
externalPath: null,
memoriesEnabled: true,
},
user1: {
user1: <UserResponseDto>{
email: 'immich@test.com',
firstName: 'immich_first_name',
id: 'user-id',
@@ -33,6 +35,7 @@ const responseDto = {
deletedAt: null,
updatedAt: new Date('2021-01-01'),
externalPath: null,
memoriesEnabled: true,
},
};

View File

@@ -1,5 +1,5 @@
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsBoolean, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { toEmail, toSanitized } from '../../domain.util';
export class CreateUserDto {
@@ -27,6 +27,10 @@ export class CreateUserDto {
@IsOptional()
@IsString()
externalPath?: string | null;
@IsOptional()
@IsBoolean()
memoriesEnabled?: boolean;
}
export class CreateAdminDto {

View File

@@ -45,4 +45,8 @@ export class UpdateUserDto {
@IsOptional()
@IsBoolean()
shouldChangePassword?: boolean;
@IsOptional()
@IsBoolean()
memoriesEnabled?: boolean;
}

View File

@@ -14,6 +14,7 @@ export class UserResponseDto {
deletedAt!: Date | null;
updatedAt!: Date;
oauthId!: string;
memoriesEnabled!: boolean;
}
export function mapUser(entity: UserEntity): UserResponseDto {
@@ -31,5 +32,6 @@ export function mapUser(entity: UserEntity): UserResponseDto {
deletedAt: entity.deletedAt,
updatedAt: entity.updatedAt,
oauthId: entity.oauthId,
memoriesEnabled: entity.memoriesEnabled,
};
}

View File

@@ -60,6 +60,7 @@ export class UserCore {
dto.externalPath = null;
}
console.log(dto.memoriesEnabled);
return this.userRepository.update(id, dto);
} catch (e) {
Logger.error(e, 'Failed to update user info');

View File

@@ -16,6 +16,7 @@ import { ICryptoRepository } from '../crypto';
import { IJobRepository, JobName } from '../job';
import { IStorageRepository } from '../storage';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserResponseDto } from './response-dto';
import { IUserRepository } from './user.repository';
import { UserService } from './user.service';
@@ -54,6 +55,7 @@ const adminUser: UserEntity = Object.freeze({
assets: [],
storageLabel: 'admin',
externalPath: null,
memoriesEnabled: true,
});
const immichUser: UserEntity = Object.freeze({
@@ -73,9 +75,10 @@ const immichUser: UserEntity = Object.freeze({
assets: [],
storageLabel: null,
externalPath: null,
memoriesEnabled: true,
});
const updatedImmichUser: UserEntity = Object.freeze({
const updatedImmichUser = Object.freeze<UserEntity>({
id: immichUserAuth.id,
email: 'immich@test.com',
password: 'immich_password',
@@ -92,9 +95,10 @@ const updatedImmichUser: UserEntity = Object.freeze({
assets: [],
storageLabel: null,
externalPath: null,
memoriesEnabled: true,
});
const adminUserResponse = Object.freeze({
const adminUserResponse = Object.freeze<UserResponseDto>({
id: adminUserAuth.id,
email: 'admin@test.com',
firstName: 'admin_first_name',
@@ -108,6 +112,7 @@ const adminUserResponse = Object.freeze({
updatedAt: new Date('2021-01-01'),
storageLabel: 'admin',
externalPath: null,
memoriesEnabled: true,
});
describe(UserService.name, () => {
@@ -158,6 +163,7 @@ describe(UserService.name, () => {
updatedAt: new Date('2021-01-01'),
storageLabel: 'admin',
externalPath: null,
memoriesEnabled: true,
},
]);
});

View File

@@ -54,6 +54,9 @@ export class UserEntity {
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt!: Date;
@Column({ default: true })
memoriesEnabled!: boolean;
@OneToMany(() => TagEntity, (tag) => tag.user)
tags!: TagEntity[];

View File

@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UserMemoryPreference1691600216749 implements MigrationInterface {
name = 'UserMemoryPreference1691600216749';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "memoriesEnabled" boolean NOT NULL DEFAULT true`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "memoriesEnabled"`);
}
}

View File

@@ -143,6 +143,24 @@ describe(`${UserController.name}`, () => {
});
expect(status).toBe(201);
});
it('should create a user without memories enabled', async () => {
const { status, body } = await request(server)
.post(`/user`)
.send({
email: 'no-memories@immich.app',
password: 'Password123',
firstName: 'No Memories',
lastName: 'User',
memoriesEnabled: false,
})
.set('Authorization', `Bearer ${accessToken}`);
expect(body).toMatchObject({
email: 'no-memories@immich.app',
memoriesEnabled: false,
});
expect(status).toBe(201);
});
});
describe('PUT /user', () => {
@@ -206,6 +224,21 @@ describe(`${UserController.name}`, () => {
});
expect(before.updatedAt).not.toEqual(after.updatedAt);
});
it('should update memories enabled', async () => {
const before = await api.userApi.get(server, accessToken, loginResponse.userId);
const after = await api.userApi.update(server, accessToken, {
id: before.id,
memoriesEnabled: false,
});
expect(after).toMatchObject({
...before,
updatedAt: expect.anything(),
memoriesEnabled: false,
});
expect(before.updatedAt).not.toEqual(after.updatedAt);
});
});
describe('GET /user/count', () => {

View File

@@ -17,6 +17,7 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
}),
user1: Object.freeze<UserEntity>({
...authStub.user1,
@@ -33,6 +34,7 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
}),
user2: Object.freeze<UserEntity>({
...authStub.user2,
@@ -49,6 +51,7 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
}),
storageLabel: Object.freeze<UserEntity>({
...authStub.user1,
@@ -65,5 +68,6 @@ export const userStub = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
}),
};