Browse Source

Task/restrict creation of tags to unique names in tags selector (#7635)

* Restrict creation of tags to unique names

* Update changelog
pull/7636/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
4a65e44f5b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 26
      apps/api/src/services/tag/tag.service.ts
  3. 6
      libs/ui/src/lib/tags-selector/tags-selector.component.html
  4. 36
      libs/ui/src/lib/tags-selector/tags-selector.component.ts

1
CHANGELOG.md

@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Restricted the creation of tags to unique names in the tags selector component
- Changed the redaction of the monetary values in impersonation mode to be based on the scopes of the access - Changed the redaction of the monetary values in impersonation mode to be based on the scopes of the access
- Deprecated the `permissions` attribute of the access in favor of the scopes - Deprecated the `permissions` attribute of the access in favor of the scopes
- Extended the `GET api/v1/access` endpoint by the scopes - Extended the `GET api/v1/access` endpoint by the scopes

26
apps/api/src/services/tag/tag.service.ts

@ -10,9 +10,13 @@ export class TagService {
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async createTag(data: Prisma.TagCreateInput) { public async createTag(data: Prisma.TagCreateInput) {
return this.prismaService.tag.create({ try {
return await this.prismaService.tag.create({
data data
}); });
} catch (error) {
throw this.getExceptionForError(error);
}
} }
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> { public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> {
@ -121,10 +125,14 @@ export class TagService {
data: Prisma.TagUpdateInput; data: Prisma.TagUpdateInput;
where: Prisma.TagWhereUniqueInput; where: Prisma.TagWhereUniqueInput;
}): Promise<Tag> { }): Promise<Tag> {
return this.prismaService.tag.update({ try {
return await this.prismaService.tag.update({
data, data,
where where
}); });
} catch (error) {
throw this.getExceptionForError(error);
}
} }
public async validateTagIds({ public async validateTagIds({
@ -180,4 +188,18 @@ export class TagService {
return this.validateTagIds({ tagIds, userId }); return this.validateTagIds({ tagIds, userId });
} }
private getExceptionForError(error: unknown) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
return new HttpException(
getReasonPhrase(StatusCodes.CONFLICT),
StatusCodes.CONFLICT
);
}
return error;
}
} }

6
libs/ui/src/lib/tags-selector/tags-selector.component.html

@ -55,12 +55,12 @@
</mat-option> </mat-option>
} }
@if (hasPermissionToCreateTag && tagInputControl.value) { @if (hasPermissionToCreateTag && tagNameToCreate()) {
<mat-option [value]="tagInputControl.value.trim()"> <mat-option [value]="tagNameToCreate()">
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="add-circle-outline" /> <ion-icon class="mr-2" name="add-circle-outline" />
<ng-container i18n>Create</ng-container> "{{ <ng-container i18n>Create</ng-container> "{{
tagInputControl.value.trim() tagNameToCreate()
}}" }}"
</span> </span>
</mat-option> </mat-option>

36
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 separatorKeysCodes: number[] = [COMMA, ENTER];
public readonly tagInputControl = new FormControl(''); public readonly tagInputControl = new FormControl('');
public readonly tagNameToCreate = signal<string | null>(null);
public readonly tagsSelected = signal<SelectedTag[]>([]); public readonly tagsSelected = signal<SelectedTag[]>([]);
private readonly tagInput = private readonly tagInput =
@ -80,8 +81,8 @@ export class GfTagsSelectorComponent
public constructor() { public constructor() {
this.tagInputControl.valueChanges this.tagInputControl.valueChanges
.pipe(takeUntilDestroyed()) .pipe(takeUntilDestroyed())
.subscribe((value) => { .subscribe(() => {
this.filteredOptions.next(this.filterTags(value ?? '')); this.updateFilters();
}); });
addIcons({ addCircleOutline, closeOutline }); addIcons({ addCircleOutline, closeOutline });
@ -161,9 +162,8 @@ export class GfTagsSelectorComponent
this.updateFilters(); this.updateFilters();
} }
private filterTags(query: string = ''): SelectedTag[] { private filterTags(query: string): SelectedTag[] {
const tags = this.tagsSelected() ?? []; const tagIds = this.getTagsSelectedAndReadOnly().map(({ id }) => {
const tagIds = [...tags, ...(this.tagsReadOnly ?? [])].map(({ id }) => {
return id; return id;
}); });
@ -179,6 +179,27 @@ export class GfTagsSelectorComponent
}); });
} }
private getTagNameToCreate(query: string): string | null {
const name = query.trim();
if (!name) {
return null;
}
const isExistingTagName = [
...(this.tagsAvailable ?? []),
...this.getTagsSelectedAndReadOnly()
].some((tag) => {
return tag.name.toLowerCase() === name.toLowerCase();
});
return isExistingTagName ? null : name;
}
private getTagsSelectedAndReadOnly(): SelectedTag[] {
return [...this.tagsSelected(), ...(this.tagsReadOnly ?? [])];
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
private onChange = (_value: SelectedTag[]): void => { private onChange = (_value: SelectedTag[]): void => {
// ControlValueAccessor onChange callback // ControlValueAccessor onChange callback
@ -189,6 +210,9 @@ export class GfTagsSelectorComponent
}; };
private updateFilters() { private updateFilters() {
this.filteredOptions.next(this.filterTags()); const query = this.tagInputControl.value ?? '';
this.filteredOptions.next(this.filterTags(query));
this.tagNameToCreate.set(this.getTagNameToCreate(query));
} }
} }

Loading…
Cancel
Save