mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-17 15:17:58 +00:00
Performance: support bulk edit without id lists (#12355)
This commit is contained in:
@@ -534,12 +534,16 @@ describe('DocumentListViewService', () => {
|
||||
})
|
||||
|
||||
it('should support select all', () => {
|
||||
documentListViewService.selectAll()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
documentListViewService.reload()
|
||||
const reloadReq = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
req.flush(full_results)
|
||||
expect(reloadReq.request.method).toEqual('GET')
|
||||
reloadReq.flush(full_results)
|
||||
|
||||
documentListViewService.selectAll()
|
||||
expect(documentListViewService.allSelected).toBeTruthy()
|
||||
expect(documentListViewService.selectedCount).toEqual(documents.length)
|
||||
expect(documentListViewService.selected.size).toEqual(documents.length)
|
||||
expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
|
||||
documentListViewService.selectNone()
|
||||
@@ -575,26 +579,62 @@ describe('DocumentListViewService', () => {
|
||||
expect(documentListViewService.isSelected(documents[3])).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support selection range reduction', () => {
|
||||
it('should clear all-selected mode when toggling a single document', () => {
|
||||
documentListViewService.reload()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||
)
|
||||
req.flush(full_results)
|
||||
|
||||
documentListViewService.selectAll()
|
||||
expect(documentListViewService.allSelected).toBeTruthy()
|
||||
|
||||
documentListViewService.toggleSelected(documents[0])
|
||||
|
||||
expect(documentListViewService.allSelected).toBeFalsy()
|
||||
expect(documentListViewService.isSelected(documents[0])).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should clear all-selected mode when selecting a range', () => {
|
||||
documentListViewService.reload()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||
)
|
||||
req.flush(full_results)
|
||||
|
||||
documentListViewService.selectAll()
|
||||
documentListViewService.toggleSelected(documents[1])
|
||||
documentListViewService.selectAll()
|
||||
expect(documentListViewService.allSelected).toBeTruthy()
|
||||
|
||||
documentListViewService.selectRangeTo(documents[3])
|
||||
|
||||
expect(documentListViewService.allSelected).toBeFalsy()
|
||||
expect(documentListViewService.isSelected(documents[1])).toBeTruthy()
|
||||
expect(documentListViewService.isSelected(documents[2])).toBeTruthy()
|
||||
expect(documentListViewService.isSelected(documents[3])).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support selection range reduction', () => {
|
||||
documentListViewService.reload()
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
req.flush(full_results)
|
||||
|
||||
documentListViewService.selectAll()
|
||||
expect(documentListViewService.selected.size).toEqual(6)
|
||||
|
||||
documentListViewService.setFilterRules(filterRules)
|
||||
httpTestingController.expectOne(
|
||||
req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=9`
|
||||
)
|
||||
const reqs = httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id&tags__id__all=9`
|
||||
)
|
||||
reqs[0].flush({
|
||||
req.flush({
|
||||
count: 3,
|
||||
results: documents.slice(0, 3),
|
||||
})
|
||||
expect(documentListViewService.allSelected).toBeTruthy()
|
||||
expect(documentListViewService.selected.size).toEqual(3)
|
||||
})
|
||||
|
||||
|
||||
@@ -80,6 +80,11 @@ export interface ListViewState {
|
||||
*/
|
||||
selected?: Set<number>
|
||||
|
||||
/**
|
||||
* True if the full filtered result set is selected.
|
||||
*/
|
||||
allSelected?: boolean
|
||||
|
||||
/**
|
||||
* The page size of the list view.
|
||||
*/
|
||||
@@ -199,6 +204,20 @@ export class DocumentListViewService {
|
||||
sortReverse: true,
|
||||
filterRules: [],
|
||||
selected: new Set<number>(),
|
||||
allSelected: false,
|
||||
}
|
||||
}
|
||||
|
||||
private syncSelectedToCurrentPage() {
|
||||
if (!this.allSelected) {
|
||||
return
|
||||
}
|
||||
|
||||
this.selected.clear()
|
||||
this.documents?.forEach((doc) => this.selected.add(doc.id))
|
||||
|
||||
if (!this.collectionSize) {
|
||||
this.selectNone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +324,7 @@ export class DocumentListViewService {
|
||||
activeListViewState.collectionSize = result.count
|
||||
activeListViewState.documents = result.results
|
||||
this.selectionData = resultWithSelectionData.selection_data ?? null
|
||||
this.syncSelectedToCurrentPage()
|
||||
|
||||
if (updateQueryParams && !this._activeSavedViewId) {
|
||||
let base = ['/documents']
|
||||
@@ -437,6 +457,20 @@ export class DocumentListViewService {
|
||||
return this.activeListViewState.selected
|
||||
}
|
||||
|
||||
get allSelected(): boolean {
|
||||
return this.activeListViewState.allSelected ?? false
|
||||
}
|
||||
|
||||
get selectedCount(): number {
|
||||
return this.allSelected
|
||||
? (this.collectionSize ?? this.selected.size)
|
||||
: this.selected.size
|
||||
}
|
||||
|
||||
get hasSelection(): boolean {
|
||||
return this.allSelected || this.selected.size > 0
|
||||
}
|
||||
|
||||
setSort(field: string, reverse: boolean) {
|
||||
this.activeListViewState.sortField = field
|
||||
this.activeListViewState.sortReverse = reverse
|
||||
@@ -591,11 +625,16 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
selectNone() {
|
||||
this.activeListViewState.allSelected = false
|
||||
this.selected.clear()
|
||||
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
|
||||
}
|
||||
|
||||
reduceSelectionToFilter() {
|
||||
if (this.allSelected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selected.size > 0) {
|
||||
this.documentService
|
||||
.listAllFilteredIds(this.filterRules)
|
||||
@@ -610,12 +649,12 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
this.documentService
|
||||
.listAllFilteredIds(this.filterRules)
|
||||
.subscribe((ids) => ids.forEach((id) => this.selected.add(id)))
|
||||
this.activeListViewState.allSelected = true
|
||||
this.syncSelectedToCurrentPage()
|
||||
}
|
||||
|
||||
selectPage() {
|
||||
this.activeListViewState.allSelected = false
|
||||
this.selected.clear()
|
||||
this.documents.forEach((doc) => {
|
||||
this.selected.add(doc.id)
|
||||
@@ -623,10 +662,13 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
isSelected(d: Document) {
|
||||
return this.selected.has(d.id)
|
||||
return this.allSelected || this.selected.has(d.id)
|
||||
}
|
||||
|
||||
toggleSelected(d: Document): void {
|
||||
if (this.allSelected) {
|
||||
this.activeListViewState.allSelected = false
|
||||
}
|
||||
if (this.selected.has(d.id)) this.selected.delete(d.id)
|
||||
else this.selected.add(d.id)
|
||||
this.rangeSelectionAnchorIndex = this.documentIndexInCurrentView(d.id)
|
||||
@@ -634,6 +676,10 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
selectRangeTo(d: Document) {
|
||||
if (this.allSelected) {
|
||||
this.activeListViewState.allSelected = false
|
||||
}
|
||||
|
||||
if (this.rangeSelectionAnchorIndex !== null) {
|
||||
const documentToIndex = this.documentIndexInCurrentView(d.id)
|
||||
const fromIndex = Math.min(
|
||||
|
||||
@@ -198,7 +198,7 @@ describe(`DocumentService`, () => {
|
||||
const content = 'both'
|
||||
const useFilenameFormatting = false
|
||||
subscription = service
|
||||
.bulkDownload(ids, content, useFilenameFormatting)
|
||||
.bulkDownload({ documents: ids }, content, useFilenameFormatting)
|
||||
.subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/bulk_download/`
|
||||
@@ -218,7 +218,9 @@ describe(`DocumentService`, () => {
|
||||
add_tags: [15],
|
||||
remove_tags: [6],
|
||||
}
|
||||
subscription = service.bulkEdit(ids, method, parameters).subscribe()
|
||||
subscription = service
|
||||
.bulkEdit({ documents: ids }, method, parameters)
|
||||
.subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/bulk_edit/`
|
||||
)
|
||||
@@ -230,9 +232,32 @@ describe(`DocumentService`, () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should call appropriate api endpoint for bulk edit with all and filters', () => {
|
||||
const method = 'modify_tags'
|
||||
const parameters = {
|
||||
add_tags: [15],
|
||||
remove_tags: [6],
|
||||
}
|
||||
const selection = {
|
||||
all: true,
|
||||
filters: { title__icontains: 'apple' },
|
||||
}
|
||||
subscription = service.bulkEdit(selection, method, parameters).subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/bulk_edit/`
|
||||
)
|
||||
expect(req.request.method).toEqual('POST')
|
||||
expect(req.request.body).toEqual({
|
||||
all: true,
|
||||
filters: { title__icontains: 'apple' },
|
||||
method,
|
||||
parameters,
|
||||
})
|
||||
})
|
||||
|
||||
it('should call appropriate api endpoint for delete documents', () => {
|
||||
const ids = [1, 2, 3]
|
||||
subscription = service.deleteDocuments(ids).subscribe()
|
||||
subscription = service.deleteDocuments({ documents: ids }).subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/delete/`
|
||||
)
|
||||
@@ -244,7 +269,7 @@ describe(`DocumentService`, () => {
|
||||
|
||||
it('should call appropriate api endpoint for reprocess documents', () => {
|
||||
const ids = [1, 2, 3]
|
||||
subscription = service.reprocessDocuments(ids).subscribe()
|
||||
subscription = service.reprocessDocuments({ documents: ids }).subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/reprocess/`
|
||||
)
|
||||
@@ -256,7 +281,7 @@ describe(`DocumentService`, () => {
|
||||
|
||||
it('should call appropriate api endpoint for rotate documents', () => {
|
||||
const ids = [1, 2, 3]
|
||||
subscription = service.rotateDocuments(ids, 90).subscribe()
|
||||
subscription = service.rotateDocuments({ documents: ids }, 90).subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/rotate/`
|
||||
)
|
||||
|
||||
@@ -68,6 +68,12 @@ export interface RemovePasswordDocumentsRequest {
|
||||
source_mode?: BulkEditSourceMode
|
||||
}
|
||||
|
||||
export interface DocumentSelectionQuery {
|
||||
documents?: number[]
|
||||
all?: boolean
|
||||
filters?: { [key: string]: any }
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -325,33 +331,37 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
||||
return this.http.get<DocumentMetadata>(url.toString())
|
||||
}
|
||||
|
||||
bulkEdit(ids: number[], method: DocumentBulkEditMethod, args: any) {
|
||||
bulkEdit(
|
||||
selection: DocumentSelectionQuery,
|
||||
method: DocumentBulkEditMethod,
|
||||
args: any
|
||||
) {
|
||||
return this.http.post(this.getResourceUrl(null, 'bulk_edit'), {
|
||||
documents: ids,
|
||||
...selection,
|
||||
method: method,
|
||||
parameters: args,
|
||||
})
|
||||
}
|
||||
|
||||
deleteDocuments(ids: number[]) {
|
||||
deleteDocuments(selection: DocumentSelectionQuery) {
|
||||
return this.http.post(this.getResourceUrl(null, 'delete'), {
|
||||
documents: ids,
|
||||
...selection,
|
||||
})
|
||||
}
|
||||
|
||||
reprocessDocuments(ids: number[]) {
|
||||
reprocessDocuments(selection: DocumentSelectionQuery) {
|
||||
return this.http.post(this.getResourceUrl(null, 'reprocess'), {
|
||||
documents: ids,
|
||||
...selection,
|
||||
})
|
||||
}
|
||||
|
||||
rotateDocuments(
|
||||
ids: number[],
|
||||
selection: DocumentSelectionQuery,
|
||||
degrees: number,
|
||||
sourceMode: BulkEditSourceMode = BulkEditSourceMode.LATEST_VERSION
|
||||
) {
|
||||
return this.http.post(this.getResourceUrl(null, 'rotate'), {
|
||||
documents: ids,
|
||||
...selection,
|
||||
degrees,
|
||||
source_mode: sourceMode,
|
||||
})
|
||||
@@ -399,14 +409,14 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
||||
}
|
||||
|
||||
bulkDownload(
|
||||
ids: number[],
|
||||
selection: DocumentSelectionQuery,
|
||||
content = 'both',
|
||||
useFilenameFormatting: boolean = false
|
||||
) {
|
||||
return this.http.post(
|
||||
this.getResourceUrl(null, 'bulk_download'),
|
||||
{
|
||||
documents: ids,
|
||||
...selection,
|
||||
content: content,
|
||||
follow_formatting: useFilenameFormatting,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user