mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-26 22:49:01 -06:00
Enhancement: support select all for management lists (#11889)
This commit is contained in:
@@ -14,6 +14,7 @@ import { SortableDirective } from 'src/app/directives/sortable.directive'
|
|||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||||
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
@@ -36,6 +37,7 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
export class CorrespondentListComponent extends ManagementListComponent<Correspondent> {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct
|
|||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
@@ -34,6 +35,7 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
export class DocumentTypeListComponent extends ManagementListComponent<DocumentType> {
|
||||||
|
|||||||
@@ -1,8 +1,39 @@
|
|||||||
<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions">
|
<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
|
<div ngbDropdown class="btn-group flex-fill d-sm-none">
|
||||||
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelectMobile" ngbDropdownToggle>
|
||||||
|
<i-bs name="text-indent-left"></i-bs>
|
||||||
|
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
||||||
|
@if (selectedObjects.size > 0) {
|
||||||
|
<pngx-clearable-badge [selected]="selectedObjects.size > 0" [number]="selectedObjects.size" (cleared)="selectNone()"></pngx-clearable-badge><span class="visually-hidden">selected</span>
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
|
<div ngbDropdownMenu aria-labelledby="dropdownSelectMobile" class="shadow">
|
||||||
|
<button ngbDropdownItem (click)="selectNone()" i18n>Select none</button>
|
||||||
|
<button ngbDropdownItem (click)="selectPage(true)" i18n>Select page</button>
|
||||||
|
<button ngbDropdownItem (click)="selectAll()" i18n>Select all</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-none d-sm-flex flex-fill me-3">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text border-0" i18n>Select:</span>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm flex-nowrap">
|
||||||
|
@if (selectedObjects.size > 0) {
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="selectNone()">
|
||||||
|
<i-bs name="slash-circle"></i-bs> <ng-container i18n>None</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="selectPage(true)">
|
||||||
|
<i-bs name="file-earmark-check"></i-bs> <ng-container i18n>Page</ng-container>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" (click)="selectAll()">
|
||||||
|
<i-bs name="check-all"></i-bs> <ng-container i18n>All</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
|
||||||
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
||||||
</button>
|
</button>
|
||||||
@@ -31,7 +62,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
<div class="form-check m-0 ms-2 me-n2">
|
<div class="form-check m-0 ms-2 me-n2">
|
||||||
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="data.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="data.length === 0" (change)="selectPage($event.target.checked); $event.stopPropagation();">
|
||||||
<label class="form-check-label" for="all-objects"></label>
|
<label class="form-check-label" for="all-objects"></label>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
@@ -163,8 +163,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const createButton = fixture.debugElement.queryAll(By.css('button'))[4]
|
component.openCreateDialog()
|
||||||
createButton.triggerEventHandler('click')
|
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
const editDialog = modal.componentInstance as EditDialogComponent<Tag>
|
const editDialog = modal.componentInstance as EditDialogComponent<Tag>
|
||||||
@@ -187,8 +186,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const editButton = fixture.debugElement.queryAll(By.css('button'))[7]
|
component.openEditDialog(tags[0])
|
||||||
editButton.triggerEventHandler('click')
|
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
const editDialog = modal.componentInstance as EditDialogComponent<Tag>
|
const editDialog = modal.componentInstance as EditDialogComponent<Tag>
|
||||||
@@ -212,8 +210,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const deleteSpy = jest.spyOn(tagService, 'delete')
|
const deleteSpy = jest.spyOn(tagService, 'delete')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8]
|
component.openDeleteDialog(tags[0])
|
||||||
deleteButton.triggerEventHandler('click')
|
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
const editDialog = modal.componentInstance as ConfirmDialogComponent
|
const editDialog = modal.componentInstance as ConfirmDialogComponent
|
||||||
@@ -279,19 +276,84 @@ describe('ManagementListComponent', () => {
|
|||||||
expect(component.page).toEqual(1)
|
expect(component.page).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support toggle all items in view', () => {
|
it('should support toggle select page in vew', () => {
|
||||||
expect(component.selectedObjects.size).toEqual(0)
|
expect(component.selectedObjects.size).toEqual(0)
|
||||||
const toggleAllSpy = jest.spyOn(component, 'toggleAll')
|
const selectPageSpy = jest.spyOn(component, 'selectPage')
|
||||||
const checkButton = fixture.debugElement.queryAll(
|
const checkButton = fixture.debugElement.queryAll(
|
||||||
By.css('input.form-check-input')
|
By.css('input.form-check-input')
|
||||||
)[0]
|
)[0]
|
||||||
checkButton.nativeElement.dispatchEvent(new Event('click'))
|
checkButton.nativeElement.dispatchEvent(new Event('change'))
|
||||||
checkButton.nativeElement.checked = true
|
checkButton.nativeElement.checked = true
|
||||||
checkButton.nativeElement.dispatchEvent(new Event('click'))
|
checkButton.nativeElement.dispatchEvent(new Event('change'))
|
||||||
expect(toggleAllSpy).toHaveBeenCalled()
|
expect(selectPageSpy).toHaveBeenCalled()
|
||||||
expect(component.selectedObjects.size).toEqual(tags.length)
|
expect(component.selectedObjects.size).toEqual(tags.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('selectNone should clear selection and reset toggle flag', () => {
|
||||||
|
component.selectedObjects = new Set([tags[0].id, tags[1].id])
|
||||||
|
component.togggleAll = true
|
||||||
|
|
||||||
|
component.selectNone()
|
||||||
|
|
||||||
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
|
expect(component.togggleAll).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectPage should select current page items or clear selection', () => {
|
||||||
|
component.selectPage(true)
|
||||||
|
expect(component.selectedObjects).toEqual(new Set(tags.map((t) => t.id)))
|
||||||
|
expect(component.togggleAll).toBe(true)
|
||||||
|
|
||||||
|
component.togggleAll = true
|
||||||
|
component.selectPage(false)
|
||||||
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
|
expect(component.togggleAll).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectAll should use all IDs when collection size exists', () => {
|
||||||
|
;(component as any).allIDs = [1, 2, 3, 4]
|
||||||
|
component.collectionSize = 4
|
||||||
|
|
||||||
|
component.selectAll()
|
||||||
|
|
||||||
|
expect(component.selectedObjects).toEqual(new Set([1, 2, 3, 4]))
|
||||||
|
expect(component.togggleAll).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectAll should clear selection when collection size is zero', () => {
|
||||||
|
component.selectedObjects = new Set([1])
|
||||||
|
component.collectionSize = 0
|
||||||
|
component.togggleAll = true
|
||||||
|
|
||||||
|
component.selectAll()
|
||||||
|
|
||||||
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
|
expect(component.togggleAll).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggleSelected should toggle object selection and update toggle state', () => {
|
||||||
|
component.toggleSelected(tags[0])
|
||||||
|
expect(component.selectedObjects.has(tags[0].id)).toBe(true)
|
||||||
|
expect(component.togggleAll).toBe(false)
|
||||||
|
|
||||||
|
component.toggleSelected(tags[1])
|
||||||
|
component.toggleSelected(tags[2])
|
||||||
|
expect(component.togggleAll).toBe(true)
|
||||||
|
|
||||||
|
component.toggleSelected(tags[1])
|
||||||
|
expect(component.selectedObjects.has(tags[1].id)).toBe(false)
|
||||||
|
expect(component.togggleAll).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('areAllPageItemsSelected should return false when page has no selectable items', () => {
|
||||||
|
component.data = []
|
||||||
|
component.selectedObjects.clear()
|
||||||
|
|
||||||
|
expect((component as any).areAllPageItemsSelected()).toBe(false)
|
||||||
|
|
||||||
|
component.data = tags
|
||||||
|
})
|
||||||
|
|
||||||
it('should support bulk edit permissions', () => {
|
it('should support bulk edit permissions', () => {
|
||||||
const bulkEditPermsSpy = jest.spyOn(tagService, 'bulk_edit_objects')
|
const bulkEditPermsSpy = jest.spyOn(tagService, 'bulk_edit_objects')
|
||||||
component.toggleSelected(tags[0])
|
component.toggleSelected(tags[0])
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
|
|
||||||
public data: T[] = []
|
public data: T[] = []
|
||||||
private unfilteredData: T[] = []
|
private unfilteredData: T[] = []
|
||||||
|
private allIDs: number[] = []
|
||||||
|
|
||||||
public page = 1
|
public page = 1
|
||||||
|
|
||||||
@@ -172,6 +173,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.unfilteredData = c.results
|
this.unfilteredData = c.results
|
||||||
this.data = this.filterData(c.results)
|
this.data = this.filterData(c.results)
|
||||||
this.collectionSize = c.all?.length ?? c.count
|
this.collectionSize = c.all?.length ?? c.count
|
||||||
|
this.allIDs = c.all
|
||||||
}),
|
}),
|
||||||
delay(100)
|
delay(100)
|
||||||
)
|
)
|
||||||
@@ -300,16 +302,6 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
return ownsAll
|
return ownsAll
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleAll(event: PointerEvent) {
|
|
||||||
const checked = (event.target as HTMLInputElement).checked
|
|
||||||
this.togggleAll = checked
|
|
||||||
if (checked) {
|
|
||||||
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
|
||||||
} else {
|
|
||||||
this.clearSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getSelectableIDs(objects: T[]): number[] {
|
protected getSelectableIDs(objects: T[]): number[] {
|
||||||
return objects.map((o) => o.id)
|
return objects.map((o) => o.id)
|
||||||
}
|
}
|
||||||
@@ -319,10 +311,38 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.selectedObjects.clear()
|
this.selectedObjects.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectNone() {
|
||||||
|
this.clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectPage(select: boolean) {
|
||||||
|
if (select) {
|
||||||
|
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
||||||
|
this.togggleAll = this.areAllPageItemsSelected()
|
||||||
|
} else {
|
||||||
|
this.clearSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll() {
|
||||||
|
if (!this.collectionSize) {
|
||||||
|
this.clearSelection()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selectedObjects = new Set(this.allIDs)
|
||||||
|
this.togggleAll = this.areAllPageItemsSelected()
|
||||||
|
}
|
||||||
|
|
||||||
toggleSelected(object) {
|
toggleSelected(object) {
|
||||||
this.selectedObjects.has(object.id)
|
this.selectedObjects.has(object.id)
|
||||||
? this.selectedObjects.delete(object.id)
|
? this.selectedObjects.delete(object.id)
|
||||||
: this.selectedObjects.add(object.id)
|
: this.selectedObjects.add(object.id)
|
||||||
|
this.togggleAll = this.areAllPageItemsSelected()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected areAllPageItemsSelected(): boolean {
|
||||||
|
const ids = this.getSelectableIDs(this.data)
|
||||||
|
return ids.length > 0 && ids.every((id) => this.selectedObjects.has(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
setPermissions() {
|
setPermissions() {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct
|
|||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
@@ -34,6 +35,7 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class StoragePathListComponent extends ManagementListComponent<StoragePath> {
|
export class StoragePathListComponent extends ManagementListComponent<StoragePath> {
|
||||||
|
|||||||
@@ -138,16 +138,12 @@ describe('TagListComponent', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component.data = [parent as any]
|
component.data = [parent as any]
|
||||||
const selectEvent = { target: { checked: true } } as unknown as PointerEvent
|
component.selectPage(true)
|
||||||
component.toggleAll(selectEvent)
|
|
||||||
|
|
||||||
expect(component.selectedObjects.has(10)).toBe(true)
|
expect(component.selectedObjects.has(10)).toBe(true)
|
||||||
expect(component.selectedObjects.has(11)).toBe(true)
|
expect(component.selectedObjects.has(11)).toBe(true)
|
||||||
|
|
||||||
const deselectEvent = {
|
component.selectPage(false)
|
||||||
target: { checked: false },
|
|
||||||
} as unknown as PointerEvent
|
|
||||||
component.toggleAll(deselectEvent)
|
|
||||||
expect(component.selectedObjects.size).toBe(0)
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct
|
|||||||
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
import { SortableDirective } from 'src/app/directives/sortable.directive'
|
||||||
import { PermissionType } from 'src/app/services/permissions.service'
|
import { PermissionType } from 'src/app/services/permissions.service'
|
||||||
import { TagService } from 'src/app/services/rest/tag.service'
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
|
import { ClearableBadgeComponent } from '../../common/clearable-badge/clearable-badge.component'
|
||||||
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||||
@@ -34,6 +35,7 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
|||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbPaginationModule,
|
NgbPaginationModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
ClearableBadgeComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TagListComponent extends ManagementListComponent<Tag> {
|
export class TagListComponent extends ManagementListComponent<Tag> {
|
||||||
|
|||||||
Reference in New Issue
Block a user