mirror of https://github.com/ghostfolio/ghostfolio
19 changed files with 650 additions and 5 deletions
@ -0,0 +1,6 @@ |
|||||
|
import { IsString } from 'class-validator'; |
||||
|
|
||||
|
export class CreateTagDto { |
||||
|
@IsString() |
||||
|
name: string; |
||||
|
} |
@ -0,0 +1,104 @@ |
|||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
||||
|
import type { RequestWithUser } from '@ghostfolio/common/types'; |
||||
|
import { |
||||
|
Body, |
||||
|
Controller, |
||||
|
Delete, |
||||
|
Get, |
||||
|
HttpException, |
||||
|
Inject, |
||||
|
Param, |
||||
|
Post, |
||||
|
Put, |
||||
|
UseGuards |
||||
|
} from '@nestjs/common'; |
||||
|
import { REQUEST } from '@nestjs/core'; |
||||
|
import { AuthGuard } from '@nestjs/passport'; |
||||
|
import { Tag } from '@prisma/client'; |
||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
||||
|
|
||||
|
import { CreateTagDto } from './create-tag.dto'; |
||||
|
import { TagService } from './tag.service'; |
||||
|
import { UpdateTagDto } from './update-tag.dto'; |
||||
|
|
||||
|
@Controller('tag') |
||||
|
export class TagController { |
||||
|
public constructor( |
||||
|
@Inject(REQUEST) private readonly request: RequestWithUser, |
||||
|
private readonly tagService: TagService |
||||
|
) {} |
||||
|
|
||||
|
@Get() |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
public async getTags() { |
||||
|
return this.tagService.getTagsWithActivityCount(); |
||||
|
} |
||||
|
|
||||
|
@Post() |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
public async createTag(@Body() data: CreateTagDto): Promise<Tag> { |
||||
|
if (!hasPermission(this.request.user.permissions, permissions.createTag)) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return this.tagService.createTag(data); |
||||
|
} |
||||
|
|
||||
|
@Put(':id') |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
public async updateTag(@Param('id') id: string, @Body() data: UpdateTagDto) { |
||||
|
if (!hasPermission(this.request.user.permissions, permissions.updateTag)) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const originalTag = await this.tagService.getTag({ |
||||
|
id |
||||
|
}); |
||||
|
|
||||
|
if (!originalTag) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return this.tagService.updateTag({ |
||||
|
data: { |
||||
|
...data |
||||
|
}, |
||||
|
where: { |
||||
|
id |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Delete(':id') |
||||
|
@UseGuards(AuthGuard('jwt')) |
||||
|
public async deleteTag(@Param('id') id: string) { |
||||
|
if (!hasPermission(this.request.user.permissions, permissions.deleteTag)) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const originalTag = await this.tagService.getTag({ |
||||
|
id |
||||
|
}); |
||||
|
|
||||
|
if (!originalTag) { |
||||
|
throw new HttpException( |
||||
|
getReasonPhrase(StatusCodes.FORBIDDEN), |
||||
|
StatusCodes.FORBIDDEN |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return this.tagService.deleteTag({ id }); |
||||
|
} |
||||
|
} |
@ -0,0 +1,13 @@ |
|||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; |
||||
|
import { Module } from '@nestjs/common'; |
||||
|
|
||||
|
import { TagController } from './tag.controller'; |
||||
|
import { TagService } from './tag.service'; |
||||
|
|
||||
|
@Module({ |
||||
|
controllers: [TagController], |
||||
|
exports: [TagService], |
||||
|
imports: [PrismaModule], |
||||
|
providers: [TagService] |
||||
|
}) |
||||
|
export class TagModule {} |
@ -0,0 +1,79 @@ |
|||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; |
||||
|
import { Injectable } from '@nestjs/common'; |
||||
|
import { Prisma, Tag } from '@prisma/client'; |
||||
|
|
||||
|
@Injectable() |
||||
|
export class TagService { |
||||
|
public constructor(private readonly prismaService: PrismaService) {} |
||||
|
|
||||
|
public async createTag(data: Prisma.TagCreateInput) { |
||||
|
return this.prismaService.tag.create({ |
||||
|
data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async deleteTag(where: Prisma.TagWhereUniqueInput): Promise<Tag> { |
||||
|
return this.prismaService.tag.delete({ where }); |
||||
|
} |
||||
|
|
||||
|
public async getTag( |
||||
|
tagWhereUniqueInput: Prisma.TagWhereUniqueInput |
||||
|
): Promise<Tag> { |
||||
|
return this.prismaService.tag.findUnique({ |
||||
|
where: tagWhereUniqueInput |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async getTags({ |
||||
|
cursor, |
||||
|
orderBy, |
||||
|
skip, |
||||
|
take, |
||||
|
where |
||||
|
}: { |
||||
|
cursor?: Prisma.TagWhereUniqueInput; |
||||
|
orderBy?: Prisma.TagOrderByWithRelationInput; |
||||
|
skip?: number; |
||||
|
take?: number; |
||||
|
where?: Prisma.TagWhereInput; |
||||
|
} = {}) { |
||||
|
return this.prismaService.tag.findMany({ |
||||
|
cursor, |
||||
|
orderBy, |
||||
|
skip, |
||||
|
take, |
||||
|
where |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async getTagsWithActivityCount() { |
||||
|
const tagsWithOrderCount = await this.prismaService.tag.findMany({ |
||||
|
include: { |
||||
|
_count: { |
||||
|
select: { orders: true } |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return tagsWithOrderCount.map(({ _count, id, name }) => { |
||||
|
return { |
||||
|
id, |
||||
|
name, |
||||
|
activityCount: _count.orders |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public async updateTag({ |
||||
|
data, |
||||
|
where |
||||
|
}: { |
||||
|
data: Prisma.TagUpdateInput; |
||||
|
where: Prisma.TagWhereUniqueInput; |
||||
|
}): Promise<Tag> { |
||||
|
return this.prismaService.tag.update({ |
||||
|
data, |
||||
|
where |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
import { IsString } from 'class-validator'; |
||||
|
|
||||
|
export class UpdateTagDto { |
||||
|
@IsString() |
||||
|
id: string; |
||||
|
|
||||
|
@IsString() |
||||
|
name: string; |
||||
|
} |
@ -0,0 +1,85 @@ |
|||||
|
<div class="container"> |
||||
|
<div class="row"> |
||||
|
<div class="col"> |
||||
|
<div class="d-flex justify-content-end"> |
||||
|
<a |
||||
|
color="primary" |
||||
|
i18n |
||||
|
mat-flat-button |
||||
|
[queryParams]="{ createTagDialog: true }" |
||||
|
[routerLink]="[]" |
||||
|
> |
||||
|
Add Tag |
||||
|
</a> |
||||
|
</div> |
||||
|
<table |
||||
|
class="gf-table w-100" |
||||
|
mat-table |
||||
|
matSort |
||||
|
matSortActive="name" |
||||
|
matSortDirection="asc" |
||||
|
[dataSource]="dataSource" |
||||
|
> |
||||
|
<ng-container matColumnDef="name"> |
||||
|
<th |
||||
|
*matHeaderCellDef |
||||
|
class="px-1" |
||||
|
mat-header-cell |
||||
|
mat-sort-header="name" |
||||
|
> |
||||
|
<ng-container i18n>Name</ng-container> |
||||
|
</th> |
||||
|
<td *matCellDef="let element" class="px-1" mat-cell> |
||||
|
{{ element.name }} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="activities"> |
||||
|
<th |
||||
|
*matHeaderCellDef |
||||
|
class="px-1" |
||||
|
mat-header-cell |
||||
|
mat-sort-header="activityCount" |
||||
|
> |
||||
|
<ng-container i18n>Activities</ng-container> |
||||
|
</th> |
||||
|
<td *matCellDef="let element" class="px-1" mat-cell> |
||||
|
{{ element.activityCount }} |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<ng-container matColumnDef="actions"> |
||||
|
<th |
||||
|
*matHeaderCellDef |
||||
|
class="px-1 text-center" |
||||
|
i18n |
||||
|
mat-header-cell |
||||
|
></th> |
||||
|
<td *matCellDef="let element" class="px-1 text-center" mat-cell> |
||||
|
<button |
||||
|
class="mx-1 no-min-width px-2" |
||||
|
mat-button |
||||
|
[matMenuTriggerFor]="tagMenu" |
||||
|
(click)="$event.stopPropagation()" |
||||
|
> |
||||
|
<ion-icon name="ellipsis-horizontal"></ion-icon> |
||||
|
</button> |
||||
|
<mat-menu #tagMenu="matMenu" xPosition="before"> |
||||
|
<button mat-menu-item (click)="onUpdateTag(element)"> |
||||
|
<ion-icon class="mr-2" name="create-outline"></ion-icon> |
||||
|
<span i18n>Edit</span> |
||||
|
</button> |
||||
|
<button mat-menu-item (click)="onDeleteTag(element.id)"> |
||||
|
<ion-icon class="mr-2" name="trash-outline"></ion-icon> |
||||
|
<span i18n>Delete</span> |
||||
|
</button> |
||||
|
</mat-menu> |
||||
|
</td> |
||||
|
</ng-container> |
||||
|
|
||||
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> |
||||
|
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
@ -0,0 +1,5 @@ |
|||||
|
@import 'apps/client/src/styles/ghostfolio-style'; |
||||
|
|
||||
|
:host { |
||||
|
display: block; |
||||
|
} |
@ -0,0 +1,199 @@ |
|||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
OnDestroy, |
||||
|
OnInit, |
||||
|
ViewChild |
||||
|
} from '@angular/core'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { MatSort } from '@angular/material/sort'; |
||||
|
import { MatTableDataSource } from '@angular/material/table'; |
||||
|
import { ActivatedRoute, Router } from '@angular/router'; |
||||
|
import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; |
||||
|
import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; |
||||
|
import { AdminService } from '@ghostfolio/client/services/admin.service'; |
||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
||||
|
import { Tag } from '@prisma/client'; |
||||
|
import { get } from 'lodash'; |
||||
|
import { DeviceDetectorService } from 'ngx-device-detector'; |
||||
|
import { Subject, takeUntil } from 'rxjs'; |
||||
|
|
||||
|
import { CreateOrUpdateTagDialog } from './create-or-update-tag-dialog/create-or-update-tag-dialog.component'; |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
selector: 'gf-admin-tag', |
||||
|
styleUrls: ['./admin-tag.component.scss'], |
||||
|
templateUrl: './admin-tag.component.html' |
||||
|
}) |
||||
|
export class AdminTagComponent implements OnInit, OnDestroy { |
||||
|
@ViewChild(MatSort) sort: MatSort; |
||||
|
|
||||
|
public dataSource: MatTableDataSource<Tag> = new MatTableDataSource(); |
||||
|
public deviceType: string; |
||||
|
public displayedColumns = ['name', 'activities', 'actions']; |
||||
|
public tags: Tag[]; |
||||
|
|
||||
|
private unsubscribeSubject = new Subject<void>(); |
||||
|
|
||||
|
public constructor( |
||||
|
private adminService: AdminService, |
||||
|
private changeDetectorRef: ChangeDetectorRef, |
||||
|
private deviceService: DeviceDetectorService, |
||||
|
private dialog: MatDialog, |
||||
|
private route: ActivatedRoute, |
||||
|
private router: Router, |
||||
|
private userService: UserService |
||||
|
) { |
||||
|
this.route.queryParams |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((params) => { |
||||
|
if (params['createTagDialog']) { |
||||
|
this.openCreateTagDialog(); |
||||
|
} else if (params['editTagDialog']) { |
||||
|
if (this.tags) { |
||||
|
const tag = this.tags.find(({ id }) => { |
||||
|
return id === params['tagId']; |
||||
|
}); |
||||
|
|
||||
|
this.openUpdateTagDialog(tag); |
||||
|
} else { |
||||
|
this.router.navigate(['.'], { relativeTo: this.route }); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType; |
||||
|
|
||||
|
this.fetchTags(); |
||||
|
} |
||||
|
|
||||
|
public onDeleteTag(aId: string) { |
||||
|
const confirmation = confirm( |
||||
|
$localize`Do you really want to delete this tag?` |
||||
|
); |
||||
|
|
||||
|
if (confirmation) { |
||||
|
this.deleteTag(aId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public onUpdateTag({ id }: Tag) { |
||||
|
this.router.navigate([], { |
||||
|
queryParams: { editTagDialog: true, tagId: id } |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.unsubscribeSubject.next(); |
||||
|
this.unsubscribeSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
private deleteTag(aId: string) { |
||||
|
this.adminService |
||||
|
.deleteTag(aId) |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe({ |
||||
|
next: () => { |
||||
|
this.userService |
||||
|
.get(true) |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(); |
||||
|
|
||||
|
this.fetchTags(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private fetchTags() { |
||||
|
this.adminService |
||||
|
.fetchTags() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((tags) => { |
||||
|
this.tags = tags; |
||||
|
this.dataSource = new MatTableDataSource(this.tags); |
||||
|
this.dataSource.sort = this.sort; |
||||
|
this.dataSource.sortingDataAccessor = get; |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private openCreateTagDialog() { |
||||
|
const dialogRef = this.dialog.open(CreateOrUpdateTagDialog, { |
||||
|
data: { |
||||
|
tag: { |
||||
|
name: null |
||||
|
} |
||||
|
}, |
||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', |
||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem' |
||||
|
}); |
||||
|
|
||||
|
dialogRef |
||||
|
.afterClosed() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((data) => { |
||||
|
const tag: CreateTagDto = data?.tag; |
||||
|
|
||||
|
if (tag) { |
||||
|
this.adminService |
||||
|
.postTag(tag) |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe({ |
||||
|
next: () => { |
||||
|
this.userService |
||||
|
.get(true) |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(); |
||||
|
|
||||
|
this.fetchTags(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
this.router.navigate(['.'], { relativeTo: this.route }); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private openUpdateTagDialog({ id, name }) { |
||||
|
const dialogRef = this.dialog.open(CreateOrUpdateTagDialog, { |
||||
|
data: { |
||||
|
tag: { |
||||
|
id, |
||||
|
name |
||||
|
} |
||||
|
}, |
||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', |
||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem' |
||||
|
}); |
||||
|
|
||||
|
dialogRef |
||||
|
.afterClosed() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((data) => { |
||||
|
const tag: UpdateTagDto = data?.tag; |
||||
|
|
||||
|
if (tag) { |
||||
|
this.adminService |
||||
|
.putTag(tag) |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe({ |
||||
|
next: () => { |
||||
|
this.userService |
||||
|
.get(true) |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(); |
||||
|
|
||||
|
this.fetchTags(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
this.router.navigate(['.'], { relativeTo: this.route }); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,26 @@ |
|||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
||||
|
import { MatButtonModule } from '@angular/material/button'; |
||||
|
import { MatMenuModule } from '@angular/material/menu'; |
||||
|
import { MatSortModule } from '@angular/material/sort'; |
||||
|
import { MatTableModule } from '@angular/material/table'; |
||||
|
import { RouterModule } from '@angular/router'; |
||||
|
|
||||
|
import { AdminTagComponent } from './admin-tag.component'; |
||||
|
import { GfCreateOrUpdateTagDialogModule } from './create-or-update-tag-dialog/create-or-update-tag-dialog.module'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [AdminTagComponent], |
||||
|
exports: [AdminTagComponent], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
GfCreateOrUpdateTagDialogModule, |
||||
|
MatButtonModule, |
||||
|
MatMenuModule, |
||||
|
MatSortModule, |
||||
|
MatTableModule, |
||||
|
RouterModule |
||||
|
], |
||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
||||
|
}) |
||||
|
export class GfAdminTagModule {} |
@ -0,0 +1,30 @@ |
|||||
|
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; |
||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
|
||||
|
import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces'; |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
host: { class: 'h-100' }, |
||||
|
selector: 'gf-create-or-update-tag-dialog', |
||||
|
styleUrls: ['./create-or-update-tag-dialog.scss'], |
||||
|
templateUrl: 'create-or-update-tag-dialog.html' |
||||
|
}) |
||||
|
export class CreateOrUpdateTagDialog { |
||||
|
private unsubscribeSubject = new Subject<void>(); |
||||
|
|
||||
|
public constructor( |
||||
|
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams, |
||||
|
public dialogRef: MatDialogRef<CreateOrUpdateTagDialog> |
||||
|
) {} |
||||
|
|
||||
|
public onCancel() { |
||||
|
this.dialogRef.close(); |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.unsubscribeSubject.next(); |
||||
|
this.unsubscribeSubject.complete(); |
||||
|
} |
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
<form #addTagForm="ngForm" class="d-flex flex-column h-100"> |
||||
|
<h1 *ngIf="data.tag.id" i18n mat-dialog-title>Update tag</h1> |
||||
|
<h1 *ngIf="!data.tag.id" i18n mat-dialog-title>Add tag</h1> |
||||
|
<div class="flex-grow-1 py-3" mat-dialog-content> |
||||
|
<div> |
||||
|
<mat-form-field appearance="outline" class="w-100"> |
||||
|
<mat-label i18n>Name</mat-label> |
||||
|
<input matInput name="name" required [(ngModel)]="data.tag.name" /> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="justify-content-end" mat-dialog-actions> |
||||
|
<button i18n mat-button (click)="onCancel()">Cancel</button> |
||||
|
<button |
||||
|
color="primary" |
||||
|
mat-flat-button |
||||
|
[disabled]="!addTagForm.form.valid" |
||||
|
[mat-dialog-close]="data" |
||||
|
> |
||||
|
<ng-container i18n>Save</ng-container> |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
@ -0,0 +1,23 @@ |
|||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; |
||||
|
import { MatButtonModule } from '@angular/material/button'; |
||||
|
import { MatDialogModule } from '@angular/material/dialog'; |
||||
|
import { MatFormFieldModule } from '@angular/material/form-field'; |
||||
|
import { MatInputModule } from '@angular/material/input'; |
||||
|
|
||||
|
import { CreateOrUpdateTagDialog } from './create-or-update-tag-dialog.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [CreateOrUpdateTagDialog], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
FormsModule, |
||||
|
MatButtonModule, |
||||
|
MatDialogModule, |
||||
|
MatFormFieldModule, |
||||
|
MatInputModule, |
||||
|
ReactiveFormsModule |
||||
|
] |
||||
|
}) |
||||
|
export class GfCreateOrUpdateTagDialogModule {} |
@ -0,0 +1,7 @@ |
|||||
|
:host { |
||||
|
display: block; |
||||
|
|
||||
|
.mat-mdc-dialog-content { |
||||
|
max-height: unset; |
||||
|
} |
||||
|
} |
@ -0,0 +1,5 @@ |
|||||
|
import { Tag } from '@prisma/client'; |
||||
|
|
||||
|
export interface CreateOrUpdateTagDialogParams { |
||||
|
tag: Tag; |
||||
|
} |
Loading…
Reference in new issue