mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-11-10 08:36:40 -05:00
Fix: add root tag filtering for tag list page consistency, fix toggle all (#11208)
This commit is contained in:
parent
ce112cda0e
commit
d718d7d29f
@ -361,4 +361,11 @@ describe('ManagementListComponent', () => {
|
|||||||
const original = component.getOriginalObject({ id: 4 } as Tag)
|
const original = component.getOriginalObject({ id: 4 } as Tag)
|
||||||
expect(original).toEqual(childTag)
|
expect(original).toEqual(childTag)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('getSelectableIDs should return flat ids when not overridden', () => {
|
||||||
|
const ids = (
|
||||||
|
ManagementListComponent.prototype as any
|
||||||
|
).getSelectableIDs.call({}, [{ id: 1 }, { id: 5 }] as any)
|
||||||
|
expect(ids).toEqual([1, 5])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -297,13 +297,19 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleAll(event: PointerEvent) {
|
toggleAll(event: PointerEvent) {
|
||||||
if ((event.target as HTMLInputElement).checked) {
|
const checked = (event.target as HTMLInputElement).checked
|
||||||
this.selectedObjects = new Set(this.data.map((o) => o.id))
|
this.togggleAll = checked
|
||||||
|
if (checked) {
|
||||||
|
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
||||||
} else {
|
} else {
|
||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getSelectableIDs(objects: T[]): number[] {
|
||||||
|
return objects.map((o) => o.id)
|
||||||
|
}
|
||||||
|
|
||||||
clearSelection() {
|
clearSelection() {
|
||||||
this.togggleAll = false
|
this.togggleAll = false
|
||||||
this.selectedObjects.clear()
|
this.selectedObjects.clear()
|
||||||
|
|||||||
@ -17,6 +17,7 @@ describe('TagListComponent', () => {
|
|||||||
let component: TagListComponent
|
let component: TagListComponent
|
||||||
let fixture: ComponentFixture<TagListComponent>
|
let fixture: ComponentFixture<TagListComponent>
|
||||||
let tagService: TagService
|
let tagService: TagService
|
||||||
|
let listFilteredSpy: jest.SpyInstance
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -39,7 +40,7 @@ describe('TagListComponent', () => {
|
|||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
tagService = TestBed.inject(TagService)
|
tagService = TestBed.inject(TagService)
|
||||||
jest.spyOn(tagService, 'listFiltered').mockReturnValue(
|
listFilteredSpy = jest.spyOn(tagService, 'listFiltered').mockReturnValue(
|
||||||
of({
|
of({
|
||||||
count: 3,
|
count: 3,
|
||||||
all: [1, 2, 3],
|
all: [1, 2, 3],
|
||||||
@ -87,4 +88,57 @@ describe('TagListComponent', () => {
|
|||||||
const filteredWithName = component.filterData(tags as any)
|
const filteredWithName = component.filterData(tags as any)
|
||||||
expect(filteredWithName.length).toBe(3)
|
expect(filteredWithName.length).toBe(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should request only parent tags when no name filter is applied', () => {
|
||||||
|
expect(tagService.listFiltered).toHaveBeenCalledWith(
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
{ is_root: true }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include child tags when a name filter is applied', () => {
|
||||||
|
listFilteredSpy.mockClear()
|
||||||
|
component['_nameFilter'] = 'Tag'
|
||||||
|
component.reloadData()
|
||||||
|
expect(tagService.listFiltered).toHaveBeenCalledWith(
|
||||||
|
1,
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'Tag',
|
||||||
|
true,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should include child tags when selecting all', () => {
|
||||||
|
const parent = {
|
||||||
|
id: 10,
|
||||||
|
name: 'Parent',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: 'Child',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
component.data = [parent as any]
|
||||||
|
const selectEvent = { target: { checked: true } } as unknown as PointerEvent
|
||||||
|
component.toggleAll(selectEvent)
|
||||||
|
|
||||||
|
expect(component.selectedObjects.has(10)).toBe(true)
|
||||||
|
expect(component.selectedObjects.has(11)).toBe(true)
|
||||||
|
|
||||||
|
const deselectEvent = {
|
||||||
|
target: { checked: false },
|
||||||
|
} as unknown as PointerEvent
|
||||||
|
component.toggleAll(deselectEvent)
|
||||||
|
expect(component.selectedObjects.size).toBe(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -61,9 +61,29 @@ export class TagListComponent extends ManagementListComponent<Tag> {
|
|||||||
return $localize`Do you really want to delete the tag "${object.name}"?`
|
return $localize`Do you really want to delete the tag "${object.name}"?`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override reloadData(extraParams: { [key: string]: any } = null) {
|
||||||
|
const params = this.nameFilter?.length
|
||||||
|
? extraParams
|
||||||
|
: { ...extraParams, is_root: true }
|
||||||
|
super.reloadData(params)
|
||||||
|
}
|
||||||
|
|
||||||
filterData(data: Tag[]) {
|
filterData(data: Tag[]) {
|
||||||
return this.nameFilter?.length
|
return this.nameFilter?.length
|
||||||
? [...data]
|
? [...data]
|
||||||
: data.filter((tag) => !tag.parent)
|
: data.filter((tag) => !tag.parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override getSelectableIDs(tags: Tag[]): number[] {
|
||||||
|
const ids: number[] = []
|
||||||
|
for (const tag of tags.filter(Boolean)) {
|
||||||
|
if (tag.id != null) {
|
||||||
|
ids.push(tag.id)
|
||||||
|
}
|
||||||
|
if (Array.isArray(tag.children) && tag.children.length) {
|
||||||
|
ids.push(...this.getSelectableIDs(tag.children))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,6 +92,12 @@ class TagFilterSet(FilterSet):
|
|||||||
"name": CHAR_KWARGS,
|
"name": CHAR_KWARGS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_root = BooleanFilter(
|
||||||
|
label="Is root tag",
|
||||||
|
field_name="tn_parent",
|
||||||
|
lookup_expr="isnull",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DocumentTypeFilterSet(FilterSet):
|
class DocumentTypeFilterSet(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@ -229,3 +229,24 @@ class TestTagHierarchy(APITestCase):
|
|||||||
assert resp_ok.status_code in (200, 202)
|
assert resp_ok.status_code in (200, 202)
|
||||||
x.refresh_from_db()
|
x.refresh_from_db()
|
||||||
assert x.parent_pk == c.id
|
assert x.parent_pk == c.id
|
||||||
|
|
||||||
|
def test_is_root_filter_returns_only_root_tags(self):
|
||||||
|
other_root = Tag.objects.create(name="Other parent")
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/tags/",
|
||||||
|
{"is_root": "true"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data["count"] == 2
|
||||||
|
|
||||||
|
returned_ids = {row["id"] for row in response.data["results"]}
|
||||||
|
assert self.child.pk not in returned_ids
|
||||||
|
assert self.parent.pk in returned_ids
|
||||||
|
assert other_root.pk in returned_ids
|
||||||
|
|
||||||
|
parent_entry = next(
|
||||||
|
row for row in response.data["results"] if row["id"] == self.parent.pk
|
||||||
|
)
|
||||||
|
assert any(child["id"] == self.child.pk for child in parent_entry["children"])
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user