mirror of
https://github.com/KevinMidboe/immich.git
synced 2026-01-04 08:15:48 +00:00
feat (server, web): Implement Archive (#2225)
* feat (server, web): add archive * chore: generate api * feat (web): add empty placeholder for archive page * chore: remove title on favorites page Duplicates sidebar selection. Two pages (Archive and Favorites) are consistent now * refactor (web): create EmptyPlaceholder component for empty pages * fixed menu close button not close: * fix (web): remove not necessary store call * test (web): simplify asset tests code * test (web): simplify asset tests code * chore (server): remove isArchived while uploading * chore (server): remove isArchived from typesense schema * chore: generate api * fix (web): delete asset from archive page * chore: change archive asset count endpoint old endpoint: /asset/archived-count-by-user-id new endpoint: /asset/stat/archive * chore: generate api --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -36,6 +36,7 @@ export interface IAssetRepository {
|
||||
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
|
||||
getAssetCountByTimeBucket(userId: string, timeBucket: TimeGroupEnum): Promise<AssetCountByTimeBucket[]>;
|
||||
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
|
||||
getArchivedAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
|
||||
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
||||
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
|
||||
getExistingAssets(
|
||||
@@ -83,26 +84,22 @@ export class AssetRepository implements IAssetRepository {
|
||||
.groupBy('asset.type')
|
||||
.getRawMany();
|
||||
|
||||
const assetCountByUserId = new AssetCountByUserIdResponseDto();
|
||||
return this.getAssetCount(items);
|
||||
}
|
||||
|
||||
// asset type to dto property mapping
|
||||
const map: Record<AssetType, keyof AssetCountByUserIdResponseDto> = {
|
||||
[AssetType.AUDIO]: 'audio',
|
||||
[AssetType.IMAGE]: 'photos',
|
||||
[AssetType.VIDEO]: 'videos',
|
||||
[AssetType.OTHER]: 'other',
|
||||
};
|
||||
async getArchivedAssetCountByUserId(ownerId: string): Promise<AssetCountByUserIdResponseDto> {
|
||||
// Get archived asset count by AssetType
|
||||
const items = await this.assetRepository
|
||||
.createQueryBuilder('asset')
|
||||
.select(`COUNT(asset.id)`, 'count')
|
||||
.addSelect(`asset.type`, 'type')
|
||||
.where('"ownerId" = :ownerId', { ownerId: ownerId })
|
||||
.andWhere('asset.isVisible = true')
|
||||
.andWhere('asset.isArchived = true')
|
||||
.groupBy('asset.type')
|
||||
.getRawMany();
|
||||
|
||||
for (const item of items) {
|
||||
const count = Number(item.count) || 0;
|
||||
const assetType = item.type as AssetType;
|
||||
const type = map[assetType];
|
||||
|
||||
assetCountByUserId[type] = count;
|
||||
assetCountByUserId.total += count;
|
||||
}
|
||||
|
||||
return assetCountByUserId;
|
||||
return this.getAssetCount(items);
|
||||
}
|
||||
|
||||
async getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]> {
|
||||
@@ -115,6 +112,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
})
|
||||
.andWhere('asset.resizePath is not NULL')
|
||||
.andWhere('asset.isVisible = true')
|
||||
.andWhere('asset.isArchived = false')
|
||||
.orderBy('asset.fileCreatedAt', 'DESC')
|
||||
.getMany();
|
||||
}
|
||||
@@ -130,6 +128,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
.where('"ownerId" = :userId', { userId: userId })
|
||||
.andWhere('asset.resizePath is not NULL')
|
||||
.andWhere('asset.isVisible = true')
|
||||
.andWhere('asset.isArchived = false')
|
||||
.groupBy(`date_trunc('month', "fileCreatedAt")`)
|
||||
.orderBy(`date_trunc('month', "fileCreatedAt")`, 'DESC')
|
||||
.getRawMany();
|
||||
@@ -141,6 +140,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
.where('"ownerId" = :userId', { userId: userId })
|
||||
.andWhere('asset.resizePath is not NULL')
|
||||
.andWhere('asset.isVisible = true')
|
||||
.andWhere('asset.isArchived = false')
|
||||
.groupBy(`date_trunc('day', "fileCreatedAt")`)
|
||||
.orderBy(`date_trunc('day', "fileCreatedAt")`, 'DESC')
|
||||
.getRawMany();
|
||||
@@ -224,6 +224,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
resizePath: Not(IsNull()),
|
||||
isVisible: true,
|
||||
isFavorite: dto.isFavorite,
|
||||
isArchived: dto.isArchived,
|
||||
},
|
||||
relations: {
|
||||
exifInfo: true,
|
||||
@@ -260,6 +261,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
*/
|
||||
async update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity> {
|
||||
asset.isFavorite = dto.isFavorite ?? asset.isFavorite;
|
||||
asset.isArchived = dto.isArchived ?? asset.isArchived;
|
||||
|
||||
if (dto.tagIds) {
|
||||
const tags = await this._tagRepository.getByIds(userId, dto.tagIds);
|
||||
@@ -330,4 +332,27 @@ export class AssetRepository implements IAssetRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private getAssetCount(items: any): AssetCountByUserIdResponseDto {
|
||||
const assetCountByUserId = new AssetCountByUserIdResponseDto();
|
||||
|
||||
// asset type to dto property mapping
|
||||
const map: Record<AssetType, keyof AssetCountByUserIdResponseDto> = {
|
||||
[AssetType.AUDIO]: 'audio',
|
||||
[AssetType.IMAGE]: 'photos',
|
||||
[AssetType.VIDEO]: 'videos',
|
||||
[AssetType.OTHER]: 'other',
|
||||
};
|
||||
|
||||
for (const item of items) {
|
||||
const count = Number(item.count) || 0;
|
||||
const assetType = item.type as AssetType;
|
||||
const type = map[assetType];
|
||||
|
||||
assetCountByUserId[type] = count;
|
||||
assetCountByUserId.total += count;
|
||||
}
|
||||
|
||||
return assetCountByUserId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +228,11 @@ export class AssetController {
|
||||
return this.assetService.getAssetCountByUserId(authUser);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Get('/stat/archive')
|
||||
async getArchivedAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this.assetService.getArchivedAssetCountByUserId(authUser);
|
||||
}
|
||||
/**
|
||||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,7 @@ export class AssetCore {
|
||||
|
||||
type: dto.assetType,
|
||||
isFavorite: dto.isFavorite,
|
||||
isArchived: dto.isArchived ?? false,
|
||||
duration: dto.duration || null,
|
||||
isVisible: dto.isVisible ?? true,
|
||||
livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null,
|
||||
|
||||
@@ -32,6 +32,7 @@ const _getCreateAssetDto = (): CreateAssetDto => {
|
||||
createAssetDto.fileCreatedAt = '2022-06-19T23:41:36.910Z';
|
||||
createAssetDto.fileModifiedAt = '2022-06-19T23:41:36.910Z';
|
||||
createAssetDto.isFavorite = false;
|
||||
createAssetDto.isArchived = false;
|
||||
createAssetDto.duration = '0:00:00.000000';
|
||||
|
||||
return createAssetDto;
|
||||
@@ -51,6 +52,7 @@ const _getAsset_1 = () => {
|
||||
asset_1.fileCreatedAt = '2022-06-19T23:41:36.910Z';
|
||||
asset_1.updatedAt = '2022-06-19T23:41:36.910Z';
|
||||
asset_1.isFavorite = false;
|
||||
asset_1.isArchived = false;
|
||||
asset_1.mimeType = 'image/jpeg';
|
||||
asset_1.webpPath = '';
|
||||
asset_1.encodedVideoPath = '';
|
||||
@@ -72,6 +74,7 @@ const _getAsset_2 = () => {
|
||||
asset_2.fileCreatedAt = '2022-06-19T23:41:36.910Z';
|
||||
asset_2.updatedAt = '2022-06-19T23:41:36.910Z';
|
||||
asset_2.isFavorite = false;
|
||||
asset_2.isArchived = false;
|
||||
asset_2.mimeType = 'image/jpeg';
|
||||
asset_2.webpPath = '';
|
||||
asset_2.encodedVideoPath = '';
|
||||
@@ -105,6 +108,15 @@ const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const _getArchivedAssetsCountByUserId = (): AssetCountByUserIdResponseDto => {
|
||||
const result = new AssetCountByUserIdResponseDto();
|
||||
|
||||
result.videos = 1;
|
||||
result.photos = 2;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
describe('AssetService', () => {
|
||||
let sut: AssetService;
|
||||
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
|
||||
@@ -136,6 +148,7 @@ describe('AssetService', () => {
|
||||
getAssetByTimeBucket: jest.fn(),
|
||||
getAssetByChecksum: jest.fn(),
|
||||
getAssetCountByUserId: jest.fn(),
|
||||
getArchivedAssetCountByUserId: jest.fn(),
|
||||
getExistingAssets: jest.fn(),
|
||||
countByIdAndUser: jest.fn(),
|
||||
};
|
||||
@@ -350,14 +363,16 @@ describe('AssetService', () => {
|
||||
|
||||
it('get asset count by user id', async () => {
|
||||
const assetCount = _getAssetCountByUserId();
|
||||
assetRepositoryMock.getAssetCountByUserId.mockResolvedValue(assetCount);
|
||||
|
||||
assetRepositoryMock.getAssetCountByUserId.mockImplementation(() =>
|
||||
Promise.resolve<AssetCountByUserIdResponseDto>(assetCount),
|
||||
);
|
||||
await expect(sut.getAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount);
|
||||
});
|
||||
|
||||
const result = await sut.getAssetCountByUserId(authStub.user1);
|
||||
it('get archived asset count by user id', async () => {
|
||||
const assetCount = _getArchivedAssetsCountByUserId();
|
||||
assetRepositoryMock.getArchivedAssetCountByUserId.mockResolvedValue(assetCount);
|
||||
|
||||
expect(result).toEqual(assetCount);
|
||||
await expect(sut.getArchivedAssetCountByUserId(authStub.user1)).resolves.toEqual(assetCount);
|
||||
});
|
||||
|
||||
describe('deleteAll', () => {
|
||||
|
||||
@@ -466,6 +466,10 @@ export class AssetService {
|
||||
return this._assetRepository.getAssetCountByUserId(authUser.id);
|
||||
}
|
||||
|
||||
getArchivedAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this._assetRepository.getArchivedAssetCountByUserId(authUser.id);
|
||||
}
|
||||
|
||||
async checkAssetsAccess(authUser: AuthUserDto, assetIds: string[], mustBeOwner = false) {
|
||||
for (const assetId of assetIds) {
|
||||
// Step 1: Check if asset is part of a public shared
|
||||
|
||||
@@ -9,6 +9,12 @@ export class AssetSearchDto {
|
||||
@Transform(toBoolean)
|
||||
isFavorite?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty()
|
||||
@IsBoolean()
|
||||
@Transform(toBoolean)
|
||||
isArchived?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
skip?: number;
|
||||
|
||||
@@ -24,6 +24,10 @@ export class CreateAssetDto {
|
||||
@IsNotEmpty()
|
||||
isFavorite!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isArchived?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isVisible?: boolean;
|
||||
|
||||
@@ -6,6 +6,10 @@ export class UpdateAssetDto {
|
||||
@IsBoolean()
|
||||
isFavorite?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isArchived?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
|
||||
Reference in New Issue
Block a user