Merge febe10dc93 into a02adbb828
commit
b725493d34
|
|
@ -12455,6 +12455,14 @@
|
|||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "untagDescendants",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
|
|
|
|||
|
|
@ -4695,14 +4695,17 @@ export function updateTag({ id, tagUpdateDto }: {
|
|||
/**
|
||||
* Untag assets
|
||||
*/
|
||||
export function untagAssets({ id, bulkIdsDto }: {
|
||||
export function untagAssets({ id, untagDescendants, bulkIdsDto }: {
|
||||
id: string;
|
||||
untagDescendants?: boolean;
|
||||
bulkIdsDto: BulkIdsDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: BulkIdResponseDto[];
|
||||
}>(`/tags/${encodeURIComponent(id)}/assets`, oazapfts.json({
|
||||
}>(`/tags/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
||||
untagDescendants
|
||||
}))}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "DELETE",
|
||||
body: bulkIdsDto
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Query, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
TagResponseDto,
|
||||
TagUpdateDto,
|
||||
TagUpsertDto,
|
||||
UntagAssetsOptionsDto,
|
||||
} from 'src/dtos/tag.dto';
|
||||
import { ApiTag, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
|
|
@ -125,7 +126,8 @@ export class TagController {
|
|||
@Auth() auth: AuthDto,
|
||||
@Body() dto: BulkIdsDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Query() { untagDescendants }: UntagAssetsOptionsDto,
|
||||
): Promise<BulkIdResponseDto[]> {
|
||||
return this.service.removeAssets(auth, id, dto);
|
||||
return this.service.removeAssets(auth, id, dto, untagDescendants);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { IsHexColor, IsNotEmpty, IsString, IsBoolean } from 'class-validator';
|
||||
import { Tag } from 'src/database';
|
||||
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
||||
|
||||
|
|
@ -41,6 +41,11 @@ export class TagBulkAssetsResponseDto {
|
|||
count!: number;
|
||||
}
|
||||
|
||||
export class UntagAssetsOptionsDto {
|
||||
@IsBoolean()
|
||||
untagDescendants?: boolean;
|
||||
}
|
||||
|
||||
export class TagResponseDto {
|
||||
id!: string;
|
||||
parentId?: string;
|
||||
|
|
|
|||
|
|
@ -128,6 +128,17 @@ export class TagRepository {
|
|||
await this.db.deleteFrom('tag_asset').where('tagId', '=', tagId).where('assetId', 'in', assetIds).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getDescendantIds(tagId: string): Promise<string[]> {
|
||||
const results = await this.db
|
||||
.selectFrom('tag_closure')
|
||||
.select('id_descendant')
|
||||
.where('id_ancestor', '=', tagId)
|
||||
.execute();
|
||||
|
||||
return results.map(({ id_descendant }) => id_descendant);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, tagIds: DummyValue.UUID }]] })
|
||||
@Chunked()
|
||||
upsertAssetIds(items: Insertable<TagAssetTable>[]) {
|
||||
|
|
|
|||
|
|
@ -272,6 +272,55 @@ describe(TagService.name, () => {
|
|||
expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']);
|
||||
});
|
||||
|
||||
it('should remove assets from parent tag but not child tags', async () => {
|
||||
mocks.tag.get.mockResolvedValue(tagStub.tag);
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.tag.removeAssetIds.mockResolvedValue();
|
||||
mocks.tag.getDescendantIds.mockResolvedValue(['tag-1', 'tag-child-1', 'tag-child-2']);
|
||||
|
||||
await expect(
|
||||
sut.removeAssets(authStub.admin, 'tag-1', {
|
||||
ids: ['asset-1', 'asset-2'],
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{ id: 'asset-1', success: true },
|
||||
{ id: 'asset-2', success: true },
|
||||
]);
|
||||
|
||||
expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.getDescendantIds).not.toHaveBeenCalled();
|
||||
expect(mocks.tag.removeAssetIds).not.toHaveBeenCalledWith('tag-child-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.removeAssetIds).not.toHaveBeenCalledWith('tag-child-2', ['asset-1', 'asset-2']);
|
||||
});
|
||||
|
||||
it('should remove assets from parent tag and all child tags when asked', async () => {
|
||||
mocks.tag.get.mockResolvedValue(tagStub.tag);
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1', 'asset-2']));
|
||||
mocks.tag.removeAssetIds.mockResolvedValue();
|
||||
mocks.tag.getDescendantIds.mockResolvedValue(['tag-1', 'tag-child-1', 'tag-child-2']);
|
||||
|
||||
await expect(
|
||||
sut.removeAssets(
|
||||
authStub.admin,
|
||||
'tag-1',
|
||||
{
|
||||
ids: ['asset-1', 'asset-2'],
|
||||
},
|
||||
true,
|
||||
),
|
||||
).resolves.toEqual([
|
||||
{ id: 'asset-1', success: true },
|
||||
{ id: 'asset-2', success: true },
|
||||
]);
|
||||
|
||||
expect(mocks.tag.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.getDescendantIds).toHaveBeenCalledWith('tag-1');
|
||||
expect(mocks.tag.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.removeAssetIds).toHaveBeenCalledWith('tag-child-1', ['asset-1', 'asset-2']);
|
||||
expect(mocks.tag.removeAssetIds).toHaveBeenCalledWith('tag-child-2', ['asset-1', 'asset-2']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleTagCleanup', () => {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export class TagService extends BaseService {
|
|||
return results;
|
||||
}
|
||||
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto, untagDescendants?: boolean): Promise<BulkIdResponseDto[]> {
|
||||
await this.requireAccess({ auth, permission: Permission.TagAsset, ids: [id] });
|
||||
|
||||
const results = await removeAssets(
|
||||
|
|
@ -123,6 +123,15 @@ export class TagService extends BaseService {
|
|||
{ parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TagDelete },
|
||||
);
|
||||
|
||||
if(untagDescendants) {
|
||||
const descendantTagIds = await this.tagRepository.getDescendantIds(id);
|
||||
for (const descendantTagId of descendantTagIds) {
|
||||
if (descendantTagId !== id) {
|
||||
await this.tagRepository.removeAssetIds(descendantTagId, dto.ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const { id: assetId, success } of results) {
|
||||
if (success) {
|
||||
await this.eventRepository.emit('AssetUntag', { assetId });
|
||||
|
|
|
|||
Loading…
Reference in New Issue