diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index abcf349d9..b69c276d0 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -10,9 +10,23 @@ export class TagService { public constructor(private readonly prismaService: PrismaService) {} public async createTag(data: Prisma.TagCreateInput) { - return this.prismaService.tag.create({ - data - }); + try { + return await this.prismaService.tag.create({ + data + }); + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === 'P2002' + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.CONFLICT), + StatusCodes.CONFLICT + ); + } + + throw error; + } } public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise { diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.html b/libs/ui/src/lib/tags-selector/tags-selector.component.html index 9bede09a7..b8a2c372d 100644 --- a/libs/ui/src/lib/tags-selector/tags-selector.component.html +++ b/libs/ui/src/lib/tags-selector/tags-selector.component.html @@ -55,12 +55,12 @@ } - @if (hasPermissionToCreateTag && tagInputControl.value) { - + @if (hasPermissionToCreateTag && tagNameToCreate()) { + Create "{{ - tagInputControl.value.trim() + tagNameToCreate() }}" diff --git a/libs/ui/src/lib/tags-selector/tags-selector.component.ts b/libs/ui/src/lib/tags-selector/tags-selector.component.ts index 0e97e12fd..172e77989 100644 --- a/libs/ui/src/lib/tags-selector/tags-selector.component.ts +++ b/libs/ui/src/lib/tags-selector/tags-selector.component.ts @@ -72,6 +72,7 @@ export class GfTagsSelectorComponent ); public readonly separatorKeysCodes: number[] = [COMMA, ENTER]; public readonly tagInputControl = new FormControl(''); + public readonly tagNameToCreate = signal(null); public readonly tagsSelected = signal([]); private readonly tagInput = @@ -81,7 +82,7 @@ export class GfTagsSelectorComponent this.tagInputControl.valueChanges .pipe(takeUntilDestroyed()) .subscribe((value) => { - this.filteredOptions.next(this.filterTags(value ?? '')); + this.updateFilters(value ?? ''); }); addIcons({ addCircleOutline, closeOutline }); @@ -179,6 +180,24 @@ export class GfTagsSelectorComponent }); } + private getTagNameToCreate(query: string = ''): string | null { + const name = query.trim(); + + if (!name) { + return null; + } + + const isTagNameTaken = [ + ...(this.tagsAvailable ?? []), + ...(this.tagsReadOnly ?? []), + ...(this.tagsSelected() ?? []) + ].some((tag) => { + return tag.name.toLowerCase() === name.toLowerCase(); + }); + + return isTagNameTaken ? null : name; + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars private onChange = (_value: SelectedTag[]): void => { // ControlValueAccessor onChange callback @@ -188,7 +207,8 @@ export class GfTagsSelectorComponent // ControlValueAccessor onTouched callback }; - private updateFilters() { - this.filteredOptions.next(this.filterTags()); + private updateFilters(query: string = '') { + this.filteredOptions.next(this.filterTags(query)); + this.tagNameToCreate.set(this.getTagNameToCreate(query)); } }