diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
index 57197d1ea..cef3690d1 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
@@ -34,7 +34,7 @@
+ *ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i)" [disabled]="disabled">
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
index b324ac6a0..79548aaf8 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
@@ -12,6 +12,7 @@ import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dro
import { MatchingModel } from 'src/app/data/matching-model'
import { Subject } from 'rxjs'
import { SelectionDataItem } from 'src/app/services/rest/document.service'
+import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
export interface ChangedItems {
itemsToAdd: MatchingModel[]
@@ -552,4 +553,13 @@ export class FilterableDropdownComponent {
// just track the index in case user uses arrows
this.keyboardIndex = index
}
+
+ hideCount(item: ObjectWithPermissions) {
+ // counts are pointless when clicking item would add to the set of docs
+ return (
+ this.selectionModel.logicalOperator === LogicalOperator.Or &&
+ this.manyToOne &&
+ this.selectionModel.get(item.id) !== ToggleableItemState.Selected
+ )
+ }
}
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
index af935b0db..b67ae736c 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.html
@@ -20,5 +20,5 @@
{{item.name}}
- {{count ?? item.document_count}}
+ {{count ?? item.document_count}}
diff --git a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
index 32ec671b1..7eb6d1b26 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component.ts
@@ -26,6 +26,9 @@ export class ToggleableDropdownButtonComponent {
@Input()
disabled: boolean = false
+ @Input()
+ hideCount: boolean = false
+
@Output()
toggle = new EventEmitter()
diff --git a/src-ui/src/app/data/results.ts b/src-ui/src/app/data/results.ts
index dbf99c5a1..d29a55567 100644
--- a/src-ui/src/app/data/results.ts
+++ b/src-ui/src/app/data/results.ts
@@ -2,4 +2,6 @@ export interface Results {
count: number
results: T[]
+
+ all: number[]
}
diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts
index 906e36a0f..6245c6632 100644
--- a/src-ui/src/app/services/document-list-view.service.ts
+++ b/src-ui/src/app/services/document-list-view.service.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'
import { ParamMap, Router } from '@angular/router'
-import { Observable } from 'rxjs'
+import { Observable, first } from 'rxjs'
import {
filterRulesDiffer,
cloneFilterRules,
@@ -230,7 +230,8 @@ export class DocumentListViewService {
activeListViewState.documents = result.results
this.documentService
- .getSelectionData(result.results.map((d) => d.id))
+ .getSelectionData(result.all)
+ .pipe(first())
.subscribe({
next: (selectionData) => {
this.selectionData = selectionData
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index 71ffe7be2..0a10b47fe 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -424,7 +424,7 @@ class DocumentSerializer(OwnedObjectSerializer, DynamicFieldsModelSerializer):
def to_representation(self, instance):
doc = super().to_representation(instance)
- if self.truncate_content:
+ if self.truncate_content and "content" in self.fields:
doc["content"] = doc.get("content")[0:550]
return doc
diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py
index 6c08b6955..fbb0b5eb5 100644
--- a/src/documents/tests/test_api.py
+++ b/src/documents/tests/test_api.py
@@ -499,21 +499,25 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
results = response.data["results"]
self.assertEqual(response.data["count"], 3)
self.assertEqual(len(results), 3)
+ self.assertCountEqual(response.data["all"], [d1.id, d2.id, d3.id])
response = self.client.get("/api/documents/?query=september")
results = response.data["results"]
self.assertEqual(response.data["count"], 1)
self.assertEqual(len(results), 1)
+ self.assertCountEqual(response.data["all"], [d3.id])
response = self.client.get("/api/documents/?query=statement")
results = response.data["results"]
self.assertEqual(response.data["count"], 2)
self.assertEqual(len(results), 2)
+ self.assertCountEqual(response.data["all"], [d2.id, d3.id])
response = self.client.get("/api/documents/?query=sfegdfg")
results = response.data["results"]
self.assertEqual(response.data["count"], 0)
self.assertEqual(len(results), 0)
+ self.assertCountEqual(response.data["all"], [])
def test_search_multi_page(self):
with AsyncWriter(index.open_index()) as writer:
@@ -1248,6 +1252,31 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
[d1.id, d3.id, d2.id],
)
+ def test_pagination_all(self):
+ """
+ GIVEN:
+ - A set of 50 documents
+ WHEN:
+ - API reuqest for document filtering
+ THEN:
+ - Results are paginated (25 items) and response["all"] returns all ids (50 items)
+ """
+ t = Tag.objects.create(name="tag")
+ docs = []
+ for i in range(50):
+ d = Document.objects.create(checksum=i, content=f"test{i}")
+ d.tags.add(t)
+ docs.append(d)
+
+ response = self.client.get(
+ f"/api/documents/?tags__id__in={t.id}",
+ )
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ results = response.data["results"]
+ self.assertEqual(len(results), 25)
+ self.assertEqual(len(response.data["all"]), 50)
+ self.assertCountEqual(response.data["all"], [d.id for d in docs])
+
def test_statistics(self):
doc1 = Document.objects.create(
title="none1",
diff --git a/src/paperless/views.py b/src/paperless/views.py
index 588b534e3..777641f75 100644
--- a/src/paperless/views.py
+++ b/src/paperless/views.py
@@ -1,4 +1,5 @@
import os
+from collections import OrderedDict
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
@@ -9,6 +10,7 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
+from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from documents.permissions import PaperlessObjectPermissions
@@ -23,6 +25,47 @@ class StandardPagination(PageNumberPagination):
page_size_query_param = "page_size"
max_page_size = 100000
+ def get_paginated_response(self, data):
+ return Response(
+ OrderedDict(
+ [
+ ("count", self.page.paginator.count),
+ ("next", self.get_next_link()),
+ ("previous", self.get_previous_link()),
+ ("all", self.get_all_result_ids()),
+ ("results", data),
+ ],
+ ),
+ )
+
+ def get_all_result_ids(self):
+ ids = []
+ if hasattr(self.page.paginator.object_list, "saved_results"):
+ results_page = self.page.paginator.object_list.saved_results[0]
+ if results_page is not None:
+ for i in range(0, len(results_page.results.docs())):
+ try:
+ fields = results_page.results.fields(i)
+ if "id" in fields:
+ ids.append(fields["id"])
+ except Exception:
+ pass
+ else:
+ for obj in self.page.paginator.object_list:
+ if hasattr(obj, "id"):
+ ids.append(obj.id)
+ elif hasattr(obj, "fields"):
+ ids.append(obj.fields()["id"])
+ return ids
+
+ def get_paginated_response_schema(self, schema):
+ response_schema = super().get_paginated_response_schema(schema)
+ response_schema["properties"]["all"] = {
+ "type": "array",
+ "example": "[1, 2, 3]",
+ }
+ return response_schema
+
class FaviconView(View):
def get(self, request, *args, **kwargs): # pragma: nocover