paperless-ngx/src-ui/src/app/services/document-list-view.service.ts

546 lines
16 KiB
TypeScript
Raw Normal View History

import { Injectable } from '@angular/core'
import { ParamMap, Router } from '@angular/router'
import { Observable, Subject, first, takeUntil } from 'rxjs'
frontend unit tests toasts component testing conditional import of angular setup-jest for vscode-jest support Update jest.config.js Create open-documents.service.spec.ts Add unit tests for all REST services settings service test Remove component from settings service test Create permissions.service.spec.ts upload documents service tests Update package.json Create toast.service.spec.ts Tasks service test Statistics widget component tests Update permissions.service.ts Create app.component.spec.ts settings component testing tasks component unit testing Management list component generic tests Some management component tests document notes component unit tests Create document-list.component.spec.ts Create save-view-config-dialog.component.spec.ts Create filter-editor.component.spec.ts small and large document cards unit testing Create bulk-editor.component.spec.ts document detail unit tests saving work on documentdetail component spec Create document-asn.component.spec.ts dashboard & widgets unit testing Fix ResizeObserver mock common component unit tests fix some merge errors Update app-frame.component.spec.ts Create page-header.component.spec.ts input component unit tests FilterableDropdownComponent unit testing and found minor errors update taskservice unit tests Edit dialogs unit tests Create date-dropdown.component.spec.ts Remove selectors from guard tests confirm dialog component tests app frame component test Miscellaneous component tests Update document-list-view.service.spec.ts directives unit tests Remove unused resizeobserver mock guard unit tests Update query-params.spec.ts try to fix flaky playwright filter rules utils & testing Interceptor unit tests Pipes unit testing Utils unit tests Update upload-documents.service.spec.ts consumer status service tests Update setup-jest.ts Create document-list-view.service.spec.ts Update app-routing.module.ts
2023-05-23 15:02:54 -07:00
import { FilterRule } from '../data/filter-rule'
import {
filterRulesDiffer,
cloneFilterRules,
isFullTextFilterRule,
frontend unit tests toasts component testing conditional import of angular setup-jest for vscode-jest support Update jest.config.js Create open-documents.service.spec.ts Add unit tests for all REST services settings service test Remove component from settings service test Create permissions.service.spec.ts upload documents service tests Update package.json Create toast.service.spec.ts Tasks service test Statistics widget component tests Update permissions.service.ts Create app.component.spec.ts settings component testing tasks component unit testing Management list component generic tests Some management component tests document notes component unit tests Create document-list.component.spec.ts Create save-view-config-dialog.component.spec.ts Create filter-editor.component.spec.ts small and large document cards unit testing Create bulk-editor.component.spec.ts document detail unit tests saving work on documentdetail component spec Create document-asn.component.spec.ts dashboard & widgets unit testing Fix ResizeObserver mock common component unit tests fix some merge errors Update app-frame.component.spec.ts Create page-header.component.spec.ts input component unit tests FilterableDropdownComponent unit testing and found minor errors update taskservice unit tests Edit dialogs unit tests Create date-dropdown.component.spec.ts Remove selectors from guard tests confirm dialog component tests app frame component test Miscellaneous component tests Update document-list-view.service.spec.ts directives unit tests Remove unused resizeobserver mock guard unit tests Update query-params.spec.ts try to fix flaky playwright filter rules utils & testing Interceptor unit tests Pipes unit testing Utils unit tests Update upload-documents.service.spec.ts consumer status service tests Update setup-jest.ts Create document-list-view.service.spec.ts Update app-routing.module.ts
2023-05-23 15:02:54 -07:00
} from '../utils/filter-rules'
2023-12-19 22:36:35 -08:00
import { Document } from '../data/document'
import { SavedView } from '../data/saved-view'
import { SETTINGS_KEYS } from '../data/ui-settings'
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
2022-08-08 00:03:15 -07:00
import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
import {
DocumentService,
DOCUMENT_SORT_FIELDS,
SelectionData,
} from './rest/document.service'
2022-05-09 11:01:45 -07:00
import { SettingsService } from './settings.service'
2020-10-27 01:10:18 +01:00
2021-05-15 18:48:39 +02:00
/**
* Captures the current state of the list view.
*/
2022-05-20 15:16:17 -07:00
export interface ListViewState {
2021-05-15 18:48:39 +02:00
/**
* Title of the document list view. Either "Documents" (localized) or the name of a saved view.
*/
title?: string
2021-05-15 18:48:39 +02:00
/**
* Current paginated list of documents displayed.
*/
2023-12-19 22:36:35 -08:00
documents?: Document[]
currentPage: number
2021-05-15 18:48:39 +02:00
/**
* Total amount of documents with the current filter rules. Used to calculate the number of pages.
*/
2022-05-20 15:16:17 -07:00
collectionSize?: number
2021-05-15 18:48:39 +02:00
/**
* Currently selected sort field.
*/
sortField: string
2021-05-15 18:48:39 +02:00
/**
* True if the list is sorted in reverse.
*/
sortReverse: boolean
2021-05-15 18:48:39 +02:00
/**
* Filter rules for the current list view.
*/
filterRules: FilterRule[]
2021-05-15 18:48:39 +02:00
/**
* Contains the IDs of all selected documents.
*/
selected?: Set<number>
}
2020-10-30 22:51:16 +01:00
/**
* This service manages the document list which is displayed using the document list view.
*
* This service also serves saved views by transparently switching between the document list
* and saved views on request. See below.
*/
2020-10-27 01:10:18 +01:00
@Injectable({
providedIn: 'root',
2020-10-27 01:10:18 +01:00
})
export class DocumentListViewService {
isReloading: boolean = false
initialized: boolean = false
error: string = null
2021-01-15 02:15:26 -08:00
rangeSelectionAnchorIndex: number
lastRangeSelectionToIndex: number
selectionData?: SelectionData
currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
2020-10-27 01:10:18 +01:00
private unsubscribeNotifier: Subject<any> = new Subject()
private listViewStates: Map<number, ListViewState> = new Map()
private _activeSavedViewId: number = null
get activeSavedViewId() {
return this._activeSavedViewId
}
get activeSavedViewTitle() {
return this.activeListViewState.title
}
2022-05-20 15:16:17 -07:00
constructor(
private documentService: DocumentService,
private settings: SettingsService,
private router: Router
) {
let documentListViewConfigJson = localStorage.getItem(
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG
)
if (documentListViewConfigJson) {
try {
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
// Remove null elements from the restored state
Object.keys(savedState).forEach((k) => {
if (savedState[k] == null) {
delete savedState[k]
}
})
//only use restored state attributes instead of defaults if they are not null
let newState = Object.assign(this.defaultListViewState(), savedState)
this.listViewStates.set(null, newState)
} catch (e) {
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
}
}
}
private defaultListViewState(): ListViewState {
return {
title: null,
documents: [],
currentPage: 1,
collectionSize: null,
sortField: 'created',
sortReverse: true,
filterRules: [],
selected: new Set<number>(),
}
}
2020-10-30 22:46:43 +01:00
private get activeListViewState() {
if (!this.listViewStates.has(this._activeSavedViewId)) {
this.listViewStates.set(
this._activeSavedViewId,
this.defaultListViewState()
)
}
return this.listViewStates.get(this._activeSavedViewId)
}
public cancelPending(): void {
this.unsubscribeNotifier.next(true)
}
2023-12-19 22:36:35 -08:00
activateSavedView(view: SavedView) {
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
if (view) {
this._activeSavedViewId = view.id
this.loadSavedView(view)
} else {
this._activeSavedViewId = null
}
}
2023-12-19 22:36:35 -08:00
activateSavedViewWithQueryParams(view: SavedView, queryParams: ParamMap) {
2022-08-08 00:03:15 -07:00
const viewState = paramsToViewState(queryParams)
2022-08-05 23:35:13 -07:00
this.activateSavedView(view)
2022-08-08 00:03:15 -07:00
this.currentPage = viewState.currentPage
2022-08-05 23:35:13 -07:00
}
2023-12-19 22:36:35 -08:00
loadSavedView(view: SavedView, closeCurrentView: boolean = false) {
if (closeCurrentView) {
this._activeSavedViewId = null
}
2022-05-20 15:16:17 -07:00
this.activeListViewState.filterRules = cloneFilterRules(view.filter_rules)
this.activeListViewState.sortField = view.sort_field
this.activeListViewState.sortReverse = view.sort_reverse
if (this._activeSavedViewId) {
this.activeListViewState.title = view.name
}
2022-05-20 15:16:17 -07:00
this.reduceSelectionToFilter()
2022-05-20 15:16:17 -07:00
if (!this.router.routerState.snapshot.url.includes('/view/')) {
this.router.navigate(['view', view.id])
2022-05-20 15:16:17 -07:00
}
}
loadFromQueryParams(queryParams: ParamMap) {
const paramsEmpty: boolean = queryParams.keys.length == 0
let newState: ListViewState = this.listViewStates.get(
this._activeSavedViewId
)
2022-08-08 00:03:15 -07:00
if (!paramsEmpty) newState = paramsToViewState(queryParams)
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
// only reload if things have changed
if (
!this.initialized ||
paramsEmpty ||
this.activeListViewState.sortField !== newState.sortField ||
this.activeListViewState.sortReverse !== newState.sortReverse ||
this.activeListViewState.currentPage !== newState.currentPage ||
filterRulesDiffer(
this.activeListViewState.filterRules,
newState.filterRules
)
) {
this.activeListViewState.filterRules = newState.filterRules
this.activeListViewState.sortField = newState.sortField
this.activeListViewState.sortReverse = newState.sortReverse
this.activeListViewState.currentPage = newState.currentPage
this.reload(null, paramsEmpty) // update the params if there arent any
}
2020-12-03 19:55:42 +01:00
}
2022-05-20 15:16:17 -07:00
reload(onFinish?, updateQueryParams: boolean = true) {
this.cancelPending()
this.isReloading = true
this.error = null
let activeListViewState = this.activeListViewState
this.documentService
.listFiltered(
activeListViewState.currentPage,
this.currentPageSize,
activeListViewState.sortField,
activeListViewState.sortReverse,
activeListViewState.filterRules,
{ truncate_content: true }
)
.pipe(takeUntil(this.unsubscribeNotifier))
2022-03-26 14:18:16 -07:00
.subscribe({
next: (result) => {
this.initialized = true
this.isReloading = false
activeListViewState.collectionSize = result.count
activeListViewState.documents = result.results
this.documentService
2023-05-06 09:54:45 -07:00
.getSelectionData(result.all)
.pipe(first())
.subscribe({
2023-05-06 09:54:45 -07:00
next: (selectionData) => {
this.selectionData = selectionData
},
error: () => {
this.selectionData = null
},
})
2022-05-20 15:16:17 -07:00
if (updateQueryParams && !this._activeSavedViewId) {
let base = ['/documents']
this.router.navigate(base, {
2022-08-08 00:03:15 -07:00
queryParams: paramsFromViewState(activeListViewState),
2022-10-26 12:48:22 -07:00
replaceUrl: !this.router.routerState.snapshot.url.includes('?'), // in case navigating from params-less /documents
2022-05-20 15:16:17 -07:00
})
2022-08-05 23:35:13 -07:00
} else if (this._activeSavedViewId) {
this.router.navigate([], {
2022-08-08 00:03:15 -07:00
queryParams: paramsFromViewState(activeListViewState, true),
2022-08-05 23:35:13 -07:00
queryParamsHandling: 'merge',
})
2022-05-20 15:16:17 -07:00
}
2020-10-27 01:10:18 +01:00
if (onFinish) {
onFinish()
}
2021-01-15 02:15:26 -08:00
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
2020-10-27 01:10:18 +01:00
},
2022-03-26 14:18:16 -07:00
error: (error) => {
this.isReloading = false
if (activeListViewState.currentPage != 1 && error.status == 404) {
2021-01-04 17:31:35 +01:00
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
activeListViewState.currentPage = 1
2020-10-27 01:10:18 +01:00
this.reload()
} else {
this.selectionData = null
let errorMessage
2022-04-07 23:06:46 -07:00
if (
typeof error.error !== 'string' &&
Object.keys(error.error).length > 0
) {
// e.g. { archive_serial_number: Array<string> }
errorMessage = Object.keys(error.error)
.map((fieldName) => {
const fieldError: Array<string> = error.error[fieldName]
return `${DOCUMENT_SORT_FIELDS.find(
(f) => f.field == fieldName
)?.name}: ${fieldError[0]}`
})
.join(', ')
} else {
errorMessage = error.error
}
this.error = errorMessage
2020-10-27 01:10:18 +01:00
}
2022-03-26 14:18:16 -07:00
},
})
2020-10-27 01:10:18 +01:00
}
set filterRules(filterRules: FilterRule[]) {
if (
!isFullTextFilterRule(filterRules) &&
this.activeListViewState.sortField == 'score'
) {
this.activeListViewState.sortField = 'created'
2021-04-03 22:19:12 +02:00
}
2021-05-15 18:48:39 +02:00
this.activeListViewState.filterRules = filterRules
this.reload()
2020-12-11 14:48:33 +01:00
this.reduceSelectionToFilter()
this.saveDocumentListView()
}
get filterRules(): FilterRule[] {
return this.activeListViewState.filterRules
}
set sortField(field: string) {
this.activeListViewState.sortField = field
this.reload()
this.saveDocumentListView()
}
get sortField(): string {
return this.activeListViewState.sortField
}
set sortReverse(reverse: boolean) {
this.activeListViewState.sortReverse = reverse
this.reload()
this.saveDocumentListView()
}
get sortReverse(): boolean {
return this.activeListViewState.sortReverse
}
get collectionSize(): number {
return this.activeListViewState.collectionSize
}
get currentPage(): number {
return this.activeListViewState.currentPage
}
set currentPage(page: number) {
2022-05-20 15:16:17 -07:00
if (this.activeListViewState.currentPage == page) return
this.activeListViewState.currentPage = page
this.reload()
2021-01-04 15:58:04 +01:00
this.saveDocumentListView()
}
2023-12-19 22:36:35 -08:00
get documents(): Document[] {
return this.activeListViewState.documents
}
get selected(): Set<number> {
return this.activeListViewState.selected
}
setSort(field: string, reverse: boolean) {
this.activeListViewState.sortField = field
this.activeListViewState.sortReverse = reverse
2021-01-04 15:58:04 +01:00
this.reload()
this.saveDocumentListView()
2021-01-04 15:58:04 +01:00
}
private saveDocumentListView() {
if (this._activeSavedViewId == null) {
let savedState: ListViewState = {
collectionSize: this.activeListViewState.collectionSize,
currentPage: this.activeListViewState.currentPage,
filterRules: this.activeListViewState.filterRules,
sortField: this.activeListViewState.sortField,
sortReverse: this.activeListViewState.sortReverse,
}
localStorage.setItem(
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
JSON.stringify(savedState)
)
}
2020-10-27 01:10:18 +01:00
}
2022-05-20 15:16:17 -07:00
quickFilter(filterRules: FilterRule[]) {
this._activeSavedViewId = null
2022-05-20 15:16:17 -07:00
this.filterRules = filterRules
this.router.navigate(['documents'])
2022-05-20 15:16:17 -07:00
}
2020-10-27 01:10:18 +01:00
getLastPage(): number {
2020-11-04 19:28:08 +01:00
return Math.ceil(this.collectionSize / this.currentPageSize)
2020-10-27 01:10:18 +01:00
}
hasNext(doc: number) {
if (this.documents) {
let index = this.documents.findIndex((d) => d.id == doc)
return (
index != -1 &&
(this.currentPage < this.getLastPage() ||
index + 1 < this.documents.length)
)
2020-10-27 01:10:18 +01:00
}
}
hasPrevious(doc: number) {
if (this.documents) {
2022-03-11 12:00:31 -08:00
let index = this.documents.findIndex((d) => d.id == doc)
return index != -1 && !(index == 0 && this.currentPage == 1)
}
}
2020-10-27 01:10:18 +01:00
getNext(currentDocId: number): Observable<number> {
return new Observable((nextDocId) => {
2020-10-27 01:10:18 +01:00
if (this.documents != null) {
let index = this.documents.findIndex((d) => d.id == currentDocId)
2020-10-27 01:10:18 +01:00
if (index != -1 && index + 1 < this.documents.length) {
nextDocId.next(this.documents[index + 1].id)
2020-10-27 01:10:18 +01:00
nextDocId.complete()
} else if (index != -1 && this.currentPage < this.getLastPage()) {
this.currentPage += 1
this.reload(() => {
nextDocId.next(this.documents[0].id)
nextDocId.complete()
})
} else {
nextDocId.complete()
}
} else {
nextDocId.complete()
}
})
}
getPrevious(currentDocId: number): Observable<number> {
2022-03-11 12:00:31 -08:00
return new Observable((prevDocId) => {
if (this.documents != null) {
2022-03-11 12:00:31 -08:00
let index = this.documents.findIndex((d) => d.id == currentDocId)
if (index != 0) {
2022-03-11 12:00:31 -08:00
prevDocId.next(this.documents[index - 1].id)
prevDocId.complete()
} else if (this.currentPage > 1) {
this.currentPage -= 1
this.reload(() => {
prevDocId.next(this.documents[this.documents.length - 1].id)
prevDocId.complete()
})
} else {
prevDocId.complete()
}
} else {
prevDocId.complete()
}
})
}
2020-11-04 19:28:08 +01:00
updatePageSize() {
2020-12-29 17:09:07 +01:00
let newPageSize = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
2020-11-04 19:28:08 +01:00
if (newPageSize != this.currentPageSize) {
this.currentPageSize = newPageSize
}
}
2020-12-11 14:48:33 +01:00
selectNone() {
this.selected.clear()
2021-01-15 02:15:26 -08:00
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
2020-12-11 14:48:33 +01:00
}
reduceSelectionToFilter() {
2020-12-11 14:48:33 +01:00
if (this.selected.size > 0) {
this.documentService
.listAllFilteredIds(this.filterRules)
.subscribe((ids) => {
for (let id of this.selected) {
if (!ids.includes(id)) {
this.selected.delete(id)
}
2020-12-11 14:48:33 +01:00
}
})
2020-12-11 14:48:33 +01:00
}
}
selectAll() {
this.documentService
.listAllFilteredIds(this.filterRules)
.subscribe((ids) => ids.forEach((id) => this.selected.add(id)))
2020-12-11 14:48:33 +01:00
}
selectPage() {
this.selected.clear()
this.documents.forEach((doc) => {
2020-12-11 14:48:33 +01:00
this.selected.add(doc.id)
})
}
2023-12-19 22:36:35 -08:00
isSelected(d: Document) {
2020-12-11 14:48:33 +01:00
return this.selected.has(d.id)
}
2023-12-19 22:36:35 -08:00
toggleSelected(d: Document): void {
if (this.selected.has(d.id)) this.selected.delete(d.id)
else this.selected.add(d.id)
2021-01-15 02:15:26 -08:00
this.rangeSelectionAnchorIndex = this.documentIndexInCurrentView(d.id)
this.lastRangeSelectionToIndex = null
}
2023-12-19 22:36:35 -08:00
selectRangeTo(d: Document) {
2021-01-15 02:15:26 -08:00
if (this.rangeSelectionAnchorIndex !== null) {
2021-01-15 01:54:33 -08:00
const documentToIndex = this.documentIndexInCurrentView(d.id)
const fromIndex = Math.min(
this.rangeSelectionAnchorIndex,
documentToIndex
)
2021-01-15 02:15:26 -08:00
const toIndex = Math.max(this.rangeSelectionAnchorIndex, documentToIndex)
2021-01-15 01:54:33 -08:00
if (this.lastRangeSelectionToIndex !== null) {
// revert the old selection
this.documents
.slice(
Math.min(
this.rangeSelectionAnchorIndex,
this.lastRangeSelectionToIndex
),
Math.max(
this.rangeSelectionAnchorIndex,
this.lastRangeSelectionToIndex
) + 1
)
.forEach((d) => {
this.selected.delete(d.id)
})
2021-01-15 01:54:33 -08:00
}
2021-01-14 14:10:23 -08:00
this.documents.slice(fromIndex, toIndex + 1).forEach((d) => {
2021-01-14 14:10:23 -08:00
this.selected.add(d.id)
})
2021-01-15 02:15:26 -08:00
this.lastRangeSelectionToIndex = documentToIndex
} else {
// e.g. shift key but was first click
2021-01-15 02:13:21 -08:00
this.toggleSelected(d)
2021-01-14 14:10:23 -08:00
}
}
documentIndexInCurrentView(documentID: number): number {
return this.documents.map((d) => d.id).indexOf(documentID)
}
2020-10-27 01:10:18 +01:00
}