mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: manual stack assets (#4198)
This commit is contained in:
parent
5ead4af2dc
commit
cf08ac7538
59 changed files with 2190 additions and 138 deletions
|
|
@ -20,6 +20,7 @@ import { Readable } from 'stream';
|
|||
import { JobName } from '../job';
|
||||
import {
|
||||
AssetStats,
|
||||
CommunicationEvent,
|
||||
IAssetRepository,
|
||||
ICommunicationRepository,
|
||||
ICryptoRepository,
|
||||
|
|
@ -636,10 +637,89 @@ describe(AssetService.name, () => {
|
|||
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
|
||||
});
|
||||
|
||||
/// Stack related
|
||||
|
||||
it('should require asset update access for parent', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'parent').mockResolvedValue(false);
|
||||
await expect(
|
||||
sut.updateAll(authStub.user1, {
|
||||
ids: ['asset-1'],
|
||||
stackParentId: 'parent',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should update parent asset when children are added', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
await sut.updateAll(authStub.user1, {
|
||||
ids: [],
|
||||
stackParentId: 'parent',
|
||||
}),
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['parent'], { stackParentId: null });
|
||||
});
|
||||
|
||||
it('should update parent asset when children are removed', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
assetMock.getByIds.mockResolvedValue([{ id: 'child-1', stackParentId: 'parent' } as AssetEntity]);
|
||||
|
||||
await sut.updateAll(authStub.user1, {
|
||||
ids: ['child-1'],
|
||||
removeParent: true,
|
||||
}),
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), { stackParentId: null });
|
||||
});
|
||||
|
||||
it('update parentId for new children', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
await sut.updateAll(authStub.user1, {
|
||||
stackParentId: 'parent',
|
||||
ids: ['child-1', 'child-2'],
|
||||
});
|
||||
|
||||
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: 'parent' });
|
||||
});
|
||||
|
||||
it('nullify parentId for remove children', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
await sut.updateAll(authStub.user1, {
|
||||
removeParent: true,
|
||||
ids: ['child-1', 'child-2'],
|
||||
});
|
||||
|
||||
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: null });
|
||||
});
|
||||
|
||||
it('merge stacks if new child has children', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
assetMock.getByIds.mockResolvedValue([
|
||||
{ id: 'child-1', stack: [{ id: 'child-2' } as AssetEntity] } as AssetEntity,
|
||||
]);
|
||||
|
||||
await sut.updateAll(authStub.user1, {
|
||||
ids: ['child-1'],
|
||||
stackParentId: 'parent',
|
||||
});
|
||||
|
||||
expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stackParentId: 'parent' });
|
||||
});
|
||||
|
||||
it('should send ws asset update event', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
await sut.updateAll(authStub.user1, {
|
||||
ids: ['asset-1'],
|
||||
stackParentId: 'parent',
|
||||
});
|
||||
|
||||
expect(communicationMock.send).toHaveBeenCalledWith(CommunicationEvent.ASSET_UPDATE, authStub.user1.id, [
|
||||
'asset-1',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteAll', () => {
|
||||
it('should required asset delete access for all ids', async () => {
|
||||
it('should require asset delete access for all ids', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||
await expect(
|
||||
sut.deleteAll(authStub.user1, {
|
||||
|
|
@ -677,7 +757,7 @@ describe(AssetService.name, () => {
|
|||
});
|
||||
|
||||
describe('restoreAll', () => {
|
||||
it('should required asset restore access for all ids', async () => {
|
||||
it('should require asset restore access for all ids', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||
await expect(
|
||||
sut.deleteAll(authStub.user1, {
|
||||
|
|
@ -757,6 +837,21 @@ describe(AssetService.name, () => {
|
|||
expect(assetMock.remove).toHaveBeenCalledWith(assetWithFace);
|
||||
});
|
||||
|
||||
it('should update stack parent if asset has stack children', async () => {
|
||||
when(assetMock.getById)
|
||||
.calledWith(assetStub.primaryImage.id)
|
||||
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
|
||||
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['stack-child-asset-2'], {
|
||||
stackParentId: 'stack-child-asset-1',
|
||||
});
|
||||
expect(assetMock.updateAll).toHaveBeenCalledWith(['stack-child-asset-1'], {
|
||||
stackParentId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not schedule delete-files job for readonly assets', async () => {
|
||||
when(assetMock.getById)
|
||||
.calledWith(assetStub.readOnly.id)
|
||||
|
|
@ -854,4 +949,70 @@ describe(AssetService.name, () => {
|
|||
expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateStackParent', () => {
|
||||
it('should require asset update access for new parent', async () => {
|
||||
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'old').mockResolvedValue(true);
|
||||
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'new').mockResolvedValue(false);
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
|
||||
await expect(
|
||||
sut.updateStackParent(authStub.user1, {
|
||||
oldParentId: 'old',
|
||||
newParentId: 'new',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should require asset read access for old parent', async () => {
|
||||
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'old').mockResolvedValue(false);
|
||||
when(accessMock.asset.hasOwnerAccess).calledWith(authStub.user1.id, 'new').mockResolvedValue(true);
|
||||
await expect(
|
||||
sut.updateStackParent(authStub.user1, {
|
||||
oldParentId: 'old',
|
||||
newParentId: 'new',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('make old parent the child of new parent', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
when(assetMock.getById)
|
||||
.calledWith(assetStub.image.id)
|
||||
.mockResolvedValue(assetStub.image as AssetEntity);
|
||||
|
||||
await sut.updateStackParent(authStub.user1, {
|
||||
oldParentId: assetStub.image.id,
|
||||
newParentId: 'new',
|
||||
});
|
||||
|
||||
expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id], { stackParentId: 'new' });
|
||||
});
|
||||
|
||||
it('remove stackParentId of new parent', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
await sut.updateStackParent(authStub.user1, {
|
||||
oldParentId: assetStub.primaryImage.id,
|
||||
newParentId: 'new',
|
||||
});
|
||||
|
||||
expect(assetMock.updateAll).toBeCalledWith(['new'], { stackParentId: null });
|
||||
});
|
||||
|
||||
it('update stackParentId of old parents children to new parent', async () => {
|
||||
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
|
||||
when(assetMock.getById)
|
||||
.calledWith(assetStub.primaryImage.id)
|
||||
.mockResolvedValue(assetStub.primaryImage as AssetEntity);
|
||||
|
||||
await sut.updateStackParent(authStub.user1, {
|
||||
oldParentId: assetStub.primaryImage.id,
|
||||
newParentId: 'new',
|
||||
});
|
||||
|
||||
expect(assetMock.updateAll).toBeCalledWith(
|
||||
[assetStub.primaryImage.id, 'stack-child-asset-1', 'stack-child-asset-2'],
|
||||
{ stackParentId: 'new' },
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import {
|
|||
TimeBucketDto,
|
||||
TrashAction,
|
||||
UpdateAssetDto,
|
||||
UpdateStackParentDto,
|
||||
mapStats,
|
||||
} from './dto';
|
||||
import {
|
||||
|
|
@ -208,7 +209,7 @@ export class AssetService {
|
|||
if (authUser.isShowMetadata) {
|
||||
return assets.map((asset) => mapAsset(asset));
|
||||
} else {
|
||||
return assets.map((asset) => mapAsset(asset, true));
|
||||
return assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -338,10 +339,29 @@ export class AssetService {
|
|||
}
|
||||
|
||||
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||
const { ids, ...options } = dto;
|
||||
const { ids, removeParent, ...options } = dto;
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
||||
|
||||
if (removeParent) {
|
||||
(options as Partial<AssetEntity>).stackParentId = null;
|
||||
const assets = await this.assetRepository.getByIds(ids);
|
||||
// This updates the updatedAt column of the parents to indicate that one of its children is removed
|
||||
// All the unique parent's -> parent is set to null
|
||||
ids.push(...new Set(assets.filter((a) => !!a.stackParentId).map((a) => a.stackParentId!)));
|
||||
} else if (options.stackParentId) {
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, options.stackParentId);
|
||||
// Merge stacks
|
||||
const assets = await this.assetRepository.getByIds(ids);
|
||||
const assetsWithChildren = assets.filter((a) => a.stack && a.stack.length > 0);
|
||||
ids.push(...assetsWithChildren.flatMap((child) => child.stack!.map((gChild) => gChild.id)));
|
||||
|
||||
// This updates the updatedAt column of the parent to indicate that a new child has been added
|
||||
await this.assetRepository.updateAll([options.stackParentId], { stackParentId: null });
|
||||
}
|
||||
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||
await this.assetRepository.updateAll(ids, options);
|
||||
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, ids);
|
||||
}
|
||||
|
||||
async handleAssetDeletionCheck() {
|
||||
|
|
@ -384,6 +404,14 @@ export class AssetService {
|
|||
);
|
||||
}
|
||||
|
||||
// Replace the parent of the stack children with a new asset
|
||||
if (asset.stack && asset.stack.length != 0) {
|
||||
const stackIds = asset.stack.map((a) => a.id);
|
||||
const newParentId = stackIds[0];
|
||||
await this.assetRepository.updateAll(stackIds.slice(1), { stackParentId: newParentId });
|
||||
await this.assetRepository.updateAll([newParentId], { stackParentId: null });
|
||||
}
|
||||
|
||||
await this.assetRepository.remove(asset);
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_ASSET, data: { ids: [asset.id] } });
|
||||
this.communicationRepository.send(CommunicationEvent.ASSET_DELETE, asset.ownerId, id);
|
||||
|
|
@ -454,6 +482,25 @@ export class AssetService {
|
|||
this.communicationRepository.send(CommunicationEvent.ASSET_RESTORE, authUser.id, ids);
|
||||
}
|
||||
|
||||
async updateStackParent(authUser: AuthUserDto, dto: UpdateStackParentDto): Promise<void> {
|
||||
const { oldParentId, newParentId } = dto;
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_READ, oldParentId);
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, newParentId);
|
||||
|
||||
const childIds: string[] = [];
|
||||
const oldParent = await this.assetRepository.getById(oldParentId);
|
||||
if (oldParent != null) {
|
||||
childIds.push(oldParent.id);
|
||||
// Get all children of old parent
|
||||
childIds.push(...(oldParent.stack?.map((a) => a.id) ?? []));
|
||||
}
|
||||
|
||||
this.communicationRepository.send(CommunicationEvent.ASSET_UPDATE, authUser.id, [...childIds, newParentId]);
|
||||
await this.assetRepository.updateAll(childIds, { stackParentId: newParentId });
|
||||
// Remove ParentId of new parent if this was previously a child of some other asset
|
||||
return this.assetRepository.updateAll([newParentId], { stackParentId: null });
|
||||
}
|
||||
|
||||
async run(authUser: AuthUserDto, dto: AssetJobsDto) {
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, dto.assetIds);
|
||||
|
||||
|
|
|
|||
9
server/src/domain/asset/dto/asset-stack.dto.ts
Normal file
9
server/src/domain/asset/dto/asset-stack.dto.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { ValidateUUID } from '../../domain.util';
|
||||
|
||||
export class UpdateStackParentDto {
|
||||
@ValidateUUID()
|
||||
oldParentId!: string;
|
||||
|
||||
@ValidateUUID()
|
||||
newParentId!: string;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Type } from 'class-transformer';
|
||||
import { IsBoolean, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { Optional } from '../../domain.util';
|
||||
import { Optional, ValidateUUID } from '../../domain.util';
|
||||
import { BulkIdsDto } from '../response-dto';
|
||||
|
||||
export class AssetBulkUpdateDto extends BulkIdsDto {
|
||||
|
|
@ -11,6 +11,14 @@ export class AssetBulkUpdateDto extends BulkIdsDto {
|
|||
@Optional()
|
||||
@IsBoolean()
|
||||
isArchived?: boolean;
|
||||
|
||||
@Optional()
|
||||
@ValidateUUID()
|
||||
stackParentId?: string;
|
||||
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
removeParent?: boolean;
|
||||
}
|
||||
|
||||
export class UpdateAssetDto {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from './asset-ids.dto';
|
||||
export * from './asset-stack.dto';
|
||||
export * from './asset-statistics.dto';
|
||||
export * from './asset.dto';
|
||||
export * from './download.dto';
|
||||
|
|
|
|||
|
|
@ -42,9 +42,20 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
people?: PersonResponseDto[];
|
||||
/**base64 encoded sha1 hash */
|
||||
checksum!: string;
|
||||
stackParentId?: string | null;
|
||||
stack?: AssetResponseDto[];
|
||||
@ApiProperty({ type: 'integer' })
|
||||
stackCount!: number;
|
||||
}
|
||||
|
||||
export function mapAsset(entity: AssetEntity, stripMetadata = false): AssetResponseDto {
|
||||
export type AssetMapOptions = {
|
||||
stripMetadata?: boolean;
|
||||
withStack?: boolean;
|
||||
};
|
||||
|
||||
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
|
||||
const { stripMetadata = false, withStack = false } = options;
|
||||
|
||||
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
||||
id: entity.id,
|
||||
type: entity.type,
|
||||
|
|
@ -87,6 +98,9 @@ export function mapAsset(entity: AssetEntity, stripMetadata = false): AssetRespo
|
|||
tags: entity.tags?.map(mapTag),
|
||||
people: entity.faces?.map(mapFace).filter((person) => !person.isHidden),
|
||||
checksum: entity.checksum.toString('base64'),
|
||||
stackParentId: entity.stackParentId,
|
||||
stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined,
|
||||
stackCount: entity.stack?.length ?? 0,
|
||||
isExternal: entity.isExternal,
|
||||
isOffline: entity.isOffline,
|
||||
isReadOnly: entity.isReadOnly,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export enum CommunicationEvent {
|
|||
UPLOAD_SUCCESS = 'on_upload_success',
|
||||
ASSET_DELETE = 'on_asset_delete',
|
||||
ASSET_TRASH = 'on_asset_trash',
|
||||
ASSET_UPDATE = 'on_asset_update',
|
||||
ASSET_RESTORE = 'on_asset_restore',
|
||||
PERSON_THUMBNAIL = 'on_person_thumbnail',
|
||||
SERVER_VERSION = 'on_server_version',
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export function mapSharedLinkWithoutMetadata(sharedLink: SharedLinkEntity): Shar
|
|||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map((asset) => mapAsset(asset, true)) as AssetResponseDto[],
|
||||
assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })) as AssetResponseDto[],
|
||||
album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
allowDownload: sharedLink.allowDownload,
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
faces: {
|
||||
person: true,
|
||||
},
|
||||
stack: true,
|
||||
},
|
||||
// We are specifically asking for this asset. Return it even if it is soft deleted
|
||||
withDeleted: true,
|
||||
|
|
@ -131,6 +132,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
relations: {
|
||||
exifInfo: true,
|
||||
tags: true,
|
||||
stack: true,
|
||||
},
|
||||
skip: dto.skip || 0,
|
||||
order: {
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ export class AssetService {
|
|||
const includeMetadata = this.getExifPermission(authUser);
|
||||
const asset = await this._assetRepository.getById(assetId);
|
||||
if (includeMetadata) {
|
||||
const data = mapAsset(asset);
|
||||
const data = mapAsset(asset, { withStack: true });
|
||||
|
||||
if (data.ownerId !== authUser.id) {
|
||||
data.people = [];
|
||||
|
|
@ -208,7 +208,7 @@ export class AssetService {
|
|||
|
||||
return data;
|
||||
} else {
|
||||
return mapAsset(asset, true);
|
||||
return mapAsset(asset, { stripMetadata: true, withStack: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
TimeBucketResponseDto,
|
||||
TrashAction,
|
||||
UpdateAssetDto as UpdateDto,
|
||||
UpdateStackParentDto,
|
||||
} from '@app/domain';
|
||||
import {
|
||||
Body,
|
||||
|
|
@ -137,6 +138,12 @@ export class AssetController {
|
|||
return this.service.handleTrashAction(authUser, TrashAction.RESTORE_ALL);
|
||||
}
|
||||
|
||||
@Put('stack/parent')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
updateStackParent(@AuthUser() authUser: AuthUserDto, @Body() dto: UpdateStackParentDto): Promise<void> {
|
||||
return this.service.updateStackParent(authUser, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
updateAsset(
|
||||
@AuthUser() authUser: AuthUserDto,
|
||||
|
|
|
|||
|
|
@ -148,6 +148,16 @@ export class AssetEntity {
|
|||
|
||||
@OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.asset)
|
||||
faces!: AssetFaceEntity[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
stackParentId?: string | null;
|
||||
|
||||
@ManyToOne(() => AssetEntity, (asset) => asset.stack, { nullable: true, onDelete: 'SET NULL', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'stackParentId' })
|
||||
stackParent?: AssetEntity | null;
|
||||
|
||||
@OneToMany(() => AssetEntity, (asset) => asset.stackParent)
|
||||
stack?: AssetEntity[];
|
||||
}
|
||||
|
||||
export enum AssetType {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddStackParentIdToAssets1695354433573 implements MigrationInterface {
|
||||
name = 'AddStackParentIdToAssets1695354433573'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD "stackParentId" uuid`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_b463c8edb01364bf2beba08ef19" FOREIGN KEY ("stackParentId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_b463c8edb01364bf2beba08ef19"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "stackParentId"`);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -112,6 +112,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
faces: {
|
||||
person: true,
|
||||
},
|
||||
stack: true,
|
||||
},
|
||||
withDeleted: true,
|
||||
});
|
||||
|
|
@ -192,6 +193,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
person: true,
|
||||
},
|
||||
library: true,
|
||||
stack: true,
|
||||
},
|
||||
// We are specifically asking for this asset. Return it even if it is soft deleted
|
||||
withDeleted: true,
|
||||
|
|
@ -538,6 +540,12 @@ export class AssetRepository implements IAssetRepository {
|
|||
.andWhere('person.id = :personId', { personId });
|
||||
}
|
||||
|
||||
// Hide stack children only in main timeline
|
||||
// Uncomment after adding support for stacked assets in web client
|
||||
// if (!isArchived && !isFavorite && !personId && !albumId && !isTrashed) {
|
||||
// builder = builder.andWhere('asset.stackParent IS NULL');
|
||||
// }
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue