Merge febe10dc93 into a02adbb828
commit
b725493d34
|
|
@ -12455,6 +12455,14 @@
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "untagDescendants",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
|
|
|
||||||
|
|
@ -4695,14 +4695,17 @@ export function updateTag({ id, tagUpdateDto }: {
|
||||||
/**
|
/**
|
||||||
* Untag assets
|
* Untag assets
|
||||||
*/
|
*/
|
||||||
export function untagAssets({ id, bulkIdsDto }: {
|
export function untagAssets({ id, untagDescendants, bulkIdsDto }: {
|
||||||
id: string;
|
id: string;
|
||||||
|
untagDescendants?: boolean;
|
||||||
bulkIdsDto: BulkIdsDto;
|
bulkIdsDto: BulkIdsDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: BulkIdResponseDto[];
|
data: BulkIdResponseDto[];
|
||||||
}>(`/tags/${encodeURIComponent(id)}/assets`, oazapfts.json({
|
}>(`/tags/${encodeURIComponent(id)}/assets${QS.query(QS.explode({
|
||||||
|
untagDescendants
|
||||||
|
}))}`, oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: bulkIdsDto
|
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 { ApiTags } from '@nestjs/swagger';
|
||||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
TagResponseDto,
|
TagResponseDto,
|
||||||
TagUpdateDto,
|
TagUpdateDto,
|
||||||
TagUpsertDto,
|
TagUpsertDto,
|
||||||
|
UntagAssetsOptionsDto,
|
||||||
} from 'src/dtos/tag.dto';
|
} from 'src/dtos/tag.dto';
|
||||||
import { ApiTag, Permission } from 'src/enum';
|
import { ApiTag, Permission } from 'src/enum';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
|
@ -125,7 +126,8 @@ export class TagController {
|
||||||
@Auth() auth: AuthDto,
|
@Auth() auth: AuthDto,
|
||||||
@Body() dto: BulkIdsDto,
|
@Body() dto: BulkIdsDto,
|
||||||
@Param() { id }: UUIDParamDto,
|
@Param() { id }: UUIDParamDto,
|
||||||
|
@Query() { untagDescendants }: UntagAssetsOptionsDto,
|
||||||
): Promise<BulkIdResponseDto[]> {
|
): 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 { 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 { Tag } from 'src/database';
|
||||||
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
|
|
@ -41,6 +41,11 @@ export class TagBulkAssetsResponseDto {
|
||||||
count!: number;
|
count!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UntagAssetsOptionsDto {
|
||||||
|
@IsBoolean()
|
||||||
|
untagDescendants?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class TagResponseDto {
|
export class TagResponseDto {
|
||||||
id!: string;
|
id!: string;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,17 @@ export class TagRepository {
|
||||||
await this.db.deleteFrom('tag_asset').where('tagId', '=', tagId).where('assetId', 'in', assetIds).execute();
|
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 }]] })
|
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, tagIds: DummyValue.UUID }]] })
|
||||||
@Chunked()
|
@Chunked()
|
||||||
upsertAssetIds(items: Insertable<TagAssetTable>[]) {
|
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.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']);
|
||||||
expect(mocks.tag.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']);
|
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', () => {
|
describe('handleTagCleanup', () => {
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export class TagService extends BaseService {
|
||||||
return results;
|
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] });
|
await this.requireAccess({ auth, permission: Permission.TagAsset, ids: [id] });
|
||||||
|
|
||||||
const results = await removeAssets(
|
const results = await removeAssets(
|
||||||
|
|
@ -123,6 +123,15 @@ export class TagService extends BaseService {
|
||||||
{ parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TagDelete },
|
{ 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) {
|
for (const { id: assetId, success } of results) {
|
||||||
if (success) {
|
if (success) {
|
||||||
await this.eventRepository.emit('AssetUntag', { assetId });
|
await this.eventRepository.emit('AssetUntag', { assetId });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue