mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Fix: add root tag filtering for tag list page consistency, fix toggle all (#11208)
This commit is contained in:
		| @@ -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"]) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon