mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-11-10 00:33:04 -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)
|
||||
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) {
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
this.selectedObjects = new Set(this.data.map((o) => o.id))
|
||||
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[] {
|
||||
return objects.map((o) => o.id)
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.togggleAll = false
|
||||
this.selectedObjects.clear()
|
||||
|
||||
@ -17,6 +17,7 @@ describe('TagListComponent', () => {
|
||||
let component: TagListComponent
|
||||
let fixture: ComponentFixture<TagListComponent>
|
||||
let tagService: TagService
|
||||
let listFilteredSpy: jest.SpyInstance
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
@ -39,7 +40,7 @@ describe('TagListComponent', () => {
|
||||
}).compileComponents()
|
||||
|
||||
tagService = TestBed.inject(TagService)
|
||||
jest.spyOn(tagService, 'listFiltered').mockReturnValue(
|
||||
listFilteredSpy = jest.spyOn(tagService, 'listFiltered').mockReturnValue(
|
||||
of({
|
||||
count: 3,
|
||||
all: [1, 2, 3],
|
||||
@ -87,4 +88,57 @@ describe('TagListComponent', () => {
|
||||
const filteredWithName = component.filterData(tags as any)
|
||||
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}"?`
|
||||
}
|
||||
|
||||
override reloadData(extraParams: { [key: string]: any } = null) {
|
||||
const params = this.nameFilter?.length
|
||||
? extraParams
|
||||
: { ...extraParams, is_root: true }
|
||||
super.reloadData(params)
|
||||
}
|
||||
|
||||
filterData(data: Tag[]) {
|
||||
return this.nameFilter?.length
|
||||
? [...data]
|
||||
: 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,
|
||||
}
|
||||
|
||||
is_root = BooleanFilter(
|
||||
label="Is root tag",
|
||||
field_name="tn_parent",
|
||||
lookup_expr="isnull",
|
||||
)
|
||||
|
||||
|
||||
class DocumentTypeFilterSet(FilterSet):
|
||||
class Meta:
|
||||
|
||||
@ -229,3 +229,24 @@ class TestTagHierarchy(APITestCase):
|
||||
assert resp_ok.status_code in (200, 202)
|
||||
x.refresh_from_db()
|
||||
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