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
- 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
- Deprecated the `permissions` attribute of the access in favor of 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 async createTag(data: Prisma.TagCreateInput) {
return this.prismaService.tag.create({
try {
return await this.prismaService.tag.create({
data
});
} catch (error) {
throw this.getExceptionForError(error);
}
}
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> {
@ -121,10 +125,14 @@ export class TagService {
data: Prisma.TagUpdateInput;
where: Prisma.TagWhereUniqueInput;
}): Promise<Tag> {
return this.prismaService.tag.update({
try {
return await this.prismaService.tag.update({
data,
where
});
} catch (error) {
throw this.getExceptionForError(error);
}
}
public async validateTagIds({
@ -180,4 +188,18 @@ export class TagService {
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>
}
@if (hasPermissionToCreateTag && tagInputControl.value) {
<mat-option [value]="tagInputControl.value.trim()">
@if (hasPermissionToCreateTag && tagNameToCreate()) {
<mat-option [value]="tagNameToCreate()">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="add-circle-outline" />
<ng-container i18n>Create</ng-container> "{{
tagInputControl.value.trim()
tagNameToCreate()
}}"
</span>
</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 tagInputControl = new FormControl('');
public readonly tagNameToCreate = signal<string | null>(null);
public readonly tagsSelected = signal<SelectedTag[]>([]);
private readonly tagInput =
@ -80,8 +81,8 @@ export class GfTagsSelectorComponent
public constructor() {
this.tagInputControl.valueChanges
.pipe(takeUntilDestroyed())
.subscribe((value) => {
this.filteredOptions.next(this.filterTags(value ?? ''));
.subscribe(() => {
this.updateFilters();
});
addIcons({ addCircleOutline, closeOutline });
@ -161,9 +162,8 @@ export class GfTagsSelectorComponent
this.updateFilters();
}
private filterTags(query: string = ''): SelectedTag[] {
const tags = this.tagsSelected() ?? [];
const tagIds = [...tags, ...(this.tagsReadOnly ?? [])].map(({ id }) => {
private filterTags(query: string): SelectedTag[] {
const tagIds = this.getTagsSelectedAndReadOnly().map(({ 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
private onChange = (_value: SelectedTag[]): void => {
// ControlValueAccessor onChange callback
@ -189,6 +210,9 @@ export class GfTagsSelectorComponent
};
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