mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-01 00:22:17 +00:00
Chorehancement: update to Angular v22, 'zoneless' / 'reactive' (#13114)
This commit is contained in:
@@ -109,6 +109,7 @@ describe('DocumentListViewService', () => {
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
router = TestBed.inject(Router)
|
||||
jest.spyOn(router, 'navigate').mockResolvedValue(true)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -705,11 +706,16 @@ describe('DocumentListViewService', () => {
|
||||
const customFields = ['custom_field_1', 'custom_field_2']
|
||||
documentListViewService.displayFields = customFields as any
|
||||
expect(documentListViewService.displayFields).toEqual(customFields)
|
||||
jest.spyOn(settingsService, 'allDisplayFields', 'get').mockReturnValue([
|
||||
const mockDisplayFields = [
|
||||
{ id: DisplayField.ADDED, name: 'Added' },
|
||||
{ id: DisplayField.TITLE, name: 'Title' },
|
||||
{ id: 'custom_field_1', name: 'Custom Field 1' },
|
||||
] as any)
|
||||
] as any
|
||||
jest
|
||||
.spyOn(settingsService.allDisplayFields, 'toString')
|
||||
.mockReturnValue(mockDisplayFields.toString() as any)
|
||||
// Mock the signal directly by overriding its value
|
||||
settingsService.allDisplayFields = jest.fn(() => mockDisplayFields) as any
|
||||
settingsService.displayFieldsInit.emit(true)
|
||||
expect(documentListViewService.displayFields).toEqual(['custom_field_1'])
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { Injectable, inject, signal } from '@angular/core'
|
||||
import { ParamMap, Router, UrlTree } from '@angular/router'
|
||||
import { Observable, Subject, takeUntil } from 'rxjs'
|
||||
import {
|
||||
@@ -118,6 +118,7 @@ export class DocumentListViewService {
|
||||
isReloading: boolean = false
|
||||
initialized: boolean = false
|
||||
error: string = null
|
||||
private readonly stateVersion = signal(0)
|
||||
|
||||
rangeSelectionAnchorIndex: number
|
||||
lastRangeSelectionToIndex: number
|
||||
@@ -132,6 +133,14 @@ export class DocumentListViewService {
|
||||
|
||||
private displayFieldsInitialized: boolean = false
|
||||
|
||||
private markChanged(): void {
|
||||
this.stateVersion.update((version) => version + 1)
|
||||
}
|
||||
|
||||
private trackState(): void {
|
||||
this.stateVersion()
|
||||
}
|
||||
|
||||
private restoreListViewState(savedState: unknown): ListViewState {
|
||||
const newState = this.defaultListViewState()
|
||||
|
||||
@@ -159,10 +168,12 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
get activeSavedViewId() {
|
||||
this.trackState()
|
||||
return this._activeSavedViewId
|
||||
}
|
||||
|
||||
get activeSavedViewTitle() {
|
||||
this.trackState()
|
||||
return this.activeListViewState.title
|
||||
}
|
||||
|
||||
@@ -184,13 +195,12 @@ export class DocumentListViewService {
|
||||
this.displayFieldsInitialized = true
|
||||
if (this.activeListViewState.displayFields) {
|
||||
this.activeListViewState.displayFields =
|
||||
this.activeListViewState.displayFields.filter(
|
||||
(field) =>
|
||||
this.settings.allDisplayFields.find((f) => f.id === field) !==
|
||||
undefined
|
||||
this.activeListViewState.displayFields.filter((field) =>
|
||||
this.settings.allDisplayFields().some((f) => f.id === field)
|
||||
)
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
this.markChanged()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -243,6 +253,7 @@ export class DocumentListViewService {
|
||||
} else {
|
||||
this._activeSavedViewId = null
|
||||
}
|
||||
this.markChanged()
|
||||
}
|
||||
|
||||
activateSavedViewWithQueryParams(view: SavedView, queryParams: ParamMap) {
|
||||
@@ -267,6 +278,7 @@ export class DocumentListViewService {
|
||||
this.activeListViewState.displayFields = view.display_fields
|
||||
|
||||
this.reduceSelectionToFilter()
|
||||
this.markChanged()
|
||||
|
||||
if (!this.router.routerState.snapshot.url.includes('/view/')) {
|
||||
this.router.navigate(['view', view.id])
|
||||
@@ -297,6 +309,7 @@ export class DocumentListViewService {
|
||||
this.activeListViewState.sortField = newState.sortField
|
||||
this.activeListViewState.sortReverse = newState.sortReverse
|
||||
this.activeListViewState.currentPage = newState.currentPage
|
||||
this.markChanged()
|
||||
this.reload(null, paramsEmpty) // update the params if there aren't any
|
||||
}
|
||||
}
|
||||
@@ -305,6 +318,7 @@ export class DocumentListViewService {
|
||||
this.cancelPending()
|
||||
this.isReloading = true
|
||||
this.error = null
|
||||
this.markChanged()
|
||||
let activeListViewState = this.activeListViewState
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
@@ -325,6 +339,7 @@ export class DocumentListViewService {
|
||||
activeListViewState.documents = result.results
|
||||
this.selectionData = resultWithSelectionData.selection_data ?? null
|
||||
this.syncSelectedToCurrentPage()
|
||||
this.markChanged()
|
||||
|
||||
if (updateQueryParams && !this._activeSavedViewId) {
|
||||
let base = ['/documents']
|
||||
@@ -343,18 +358,20 @@ export class DocumentListViewService {
|
||||
onFinish()
|
||||
}
|
||||
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
|
||||
this.markChanged()
|
||||
},
|
||||
error: (error) => {
|
||||
this.isReloading = false
|
||||
if (activeListViewState.currentPage != 1 && error.status == 404) {
|
||||
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
|
||||
activeListViewState.currentPage = 1
|
||||
this.markChanged()
|
||||
this.reload()
|
||||
} else if (
|
||||
activeListViewState.sortField.indexOf('custom_field') === 0 &&
|
||||
this.settings.allDisplayFields.find(
|
||||
(f) => f.id === activeListViewState.sortField
|
||||
) === undefined
|
||||
!this.settings
|
||||
.allDisplayFields()
|
||||
.some((f) => f.id === activeListViewState.sortField)
|
||||
) {
|
||||
// e.g. field was deleted
|
||||
this.sortField = 'created'
|
||||
@@ -381,6 +398,7 @@ export class DocumentListViewService {
|
||||
errorMessage = error.error
|
||||
}
|
||||
this.error = errorMessage
|
||||
this.markChanged()
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -397,12 +415,14 @@ export class DocumentListViewService {
|
||||
if (resetPage) {
|
||||
this.activeListViewState.currentPage = 1
|
||||
}
|
||||
this.markChanged()
|
||||
this.reload()
|
||||
this.reduceSelectionToFilter()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get filterRules(): FilterRule[] {
|
||||
this.trackState()
|
||||
return this.activeListViewState.filterRules
|
||||
}
|
||||
|
||||
@@ -416,48 +436,58 @@ export class DocumentListViewService {
|
||||
|
||||
set sortField(field: string) {
|
||||
this.activeListViewState.sortField = field
|
||||
this.markChanged()
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get sortField(): string {
|
||||
this.trackState()
|
||||
return this.activeListViewState.sortField
|
||||
}
|
||||
|
||||
set sortReverse(reverse: boolean) {
|
||||
this.activeListViewState.sortReverse = reverse
|
||||
this.markChanged()
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get sortReverse(): boolean {
|
||||
this.trackState()
|
||||
return this.activeListViewState.sortReverse
|
||||
}
|
||||
|
||||
get collectionSize(): number {
|
||||
this.trackState()
|
||||
return this.activeListViewState.collectionSize
|
||||
}
|
||||
|
||||
get currentPage(): number {
|
||||
this.trackState()
|
||||
return this.activeListViewState.currentPage
|
||||
}
|
||||
|
||||
set currentPage(page: number) {
|
||||
if (this.activeListViewState.currentPage == page) return
|
||||
this.activeListViewState.currentPage = page
|
||||
this.markChanged()
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get documents(): Document[] {
|
||||
this.trackState()
|
||||
return this.activeListViewState.documents
|
||||
}
|
||||
|
||||
get selected(): Set<number> {
|
||||
this.trackState()
|
||||
return this.activeListViewState.selected
|
||||
}
|
||||
|
||||
get allSelected(): boolean {
|
||||
this.trackState()
|
||||
return this.activeListViewState.allSelected ?? false
|
||||
}
|
||||
|
||||
@@ -474,16 +504,19 @@ export class DocumentListViewService {
|
||||
setSort(field: string, reverse: boolean) {
|
||||
this.activeListViewState.sortField = field
|
||||
this.activeListViewState.sortReverse = reverse
|
||||
this.markChanged()
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
set displayMode(mode: DisplayMode) {
|
||||
this.activeListViewState.displayMode = mode
|
||||
this.markChanged()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get displayMode(): DisplayMode {
|
||||
this.trackState()
|
||||
const mode = this.activeListViewState.displayMode ?? DisplayMode.SMALL_CARDS
|
||||
if (mode === ('details' as any)) {
|
||||
// legacy
|
||||
@@ -494,11 +527,13 @@ export class DocumentListViewService {
|
||||
|
||||
set pageSize(size: number) {
|
||||
this.activeListViewState.pageSize = size
|
||||
this.markChanged()
|
||||
this.reload()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
get pageSize(): number {
|
||||
this.trackState()
|
||||
return (
|
||||
this.activeListViewState.pageSize ??
|
||||
this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||
@@ -506,17 +541,17 @@ export class DocumentListViewService {
|
||||
}
|
||||
|
||||
get displayFields(): DisplayField[] {
|
||||
this.trackState()
|
||||
return this.activeListViewState.displayFields ?? LIST_DEFAULT_DISPLAY_FIELDS
|
||||
}
|
||||
|
||||
set displayFields(fields: DisplayField[]) {
|
||||
this.activeListViewState.displayFields = this.displayFieldsInitialized
|
||||
? fields?.filter(
|
||||
(field) =>
|
||||
this.settings.allDisplayFields.find((f) => f.id === field) !==
|
||||
undefined
|
||||
? fields?.filter((field) =>
|
||||
this.settings.allDisplayFields().some((f) => f.id === field)
|
||||
)
|
||||
: fields
|
||||
this.markChanged()
|
||||
this.saveDocumentListView()
|
||||
}
|
||||
|
||||
@@ -540,6 +575,7 @@ export class DocumentListViewService {
|
||||
|
||||
quickFilter(filterRules: FilterRule[]) {
|
||||
this._activeSavedViewId = null
|
||||
this.markChanged()
|
||||
this.setFilterRules(filterRules)
|
||||
this.router.navigate(['documents'])
|
||||
}
|
||||
@@ -628,6 +664,7 @@ export class DocumentListViewService {
|
||||
this.activeListViewState.allSelected = false
|
||||
this.selected.clear()
|
||||
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
|
||||
this.markChanged()
|
||||
}
|
||||
|
||||
reduceSelectionToFilter() {
|
||||
@@ -644,6 +681,7 @@ export class DocumentListViewService {
|
||||
this.selected.delete(id)
|
||||
}
|
||||
}
|
||||
this.markChanged()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -651,6 +689,7 @@ export class DocumentListViewService {
|
||||
selectAll() {
|
||||
this.activeListViewState.allSelected = true
|
||||
this.syncSelectedToCurrentPage()
|
||||
this.markChanged()
|
||||
}
|
||||
|
||||
selectPage() {
|
||||
@@ -659,6 +698,7 @@ export class DocumentListViewService {
|
||||
this.documents.forEach((doc) => {
|
||||
this.selected.add(doc.id)
|
||||
})
|
||||
this.markChanged()
|
||||
}
|
||||
|
||||
isSelected(d: Document) {
|
||||
@@ -673,6 +713,7 @@ export class DocumentListViewService {
|
||||
else this.selected.add(d.id)
|
||||
this.rangeSelectionAnchorIndex = this.documentIndexInCurrentView(d.id)
|
||||
this.lastRangeSelectionToIndex = null
|
||||
this.markChanged()
|
||||
}
|
||||
|
||||
selectRangeTo(d: Document) {
|
||||
@@ -710,6 +751,7 @@ export class DocumentListViewService {
|
||||
this.selected.add(d.id)
|
||||
})
|
||||
this.lastRangeSelectionToIndex = documentToIndex
|
||||
this.markChanged()
|
||||
} else {
|
||||
// e.g. shift key but was first click
|
||||
this.toggleSelected(d)
|
||||
|
||||
@@ -94,6 +94,6 @@ export class HotKeyService {
|
||||
|
||||
private openHelpModal() {
|
||||
const modal = this.modalService.open(HotkeyDialogComponent)
|
||||
modal.componentInstance.hotkeys = this.hotkeys
|
||||
modal.componentInstance.hotkeys.set(this.hotkeys)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { Injectable, inject, signal } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Observable, Subject, of } from 'rxjs'
|
||||
import { first } from 'rxjs/operators'
|
||||
@@ -15,6 +15,7 @@ export class OpenDocumentsService {
|
||||
private modalService = inject(NgbModal)
|
||||
|
||||
private MAX_OPEN_DOCUMENTS = 5
|
||||
private readonly stateVersion = signal(0)
|
||||
|
||||
constructor() {
|
||||
this.load()
|
||||
@@ -36,6 +37,14 @@ export class OpenDocumentsService {
|
||||
private openDocuments: Document[] = []
|
||||
private dirtyDocuments: Set<number> = new Set<number>()
|
||||
|
||||
private trackState(): void {
|
||||
this.stateVersion()
|
||||
}
|
||||
|
||||
private markChanged(): void {
|
||||
this.stateVersion.update((version) => version + 1)
|
||||
}
|
||||
|
||||
refreshDocument(id: number) {
|
||||
let index = this.openDocuments.findIndex((doc) => doc.id == id)
|
||||
if (index > -1) {
|
||||
@@ -53,10 +62,12 @@ export class OpenDocumentsService {
|
||||
}
|
||||
|
||||
getOpenDocuments(): Document[] {
|
||||
this.trackState()
|
||||
return this.openDocuments
|
||||
}
|
||||
|
||||
getOpenDocument(id: number): Document {
|
||||
this.trackState()
|
||||
return this.openDocuments.find((d) => d.id == id)
|
||||
}
|
||||
|
||||
@@ -101,10 +112,12 @@ export class OpenDocumentsService {
|
||||
}
|
||||
|
||||
hasDirty(): boolean {
|
||||
this.trackState()
|
||||
return this.dirtyDocuments.size > 0
|
||||
}
|
||||
|
||||
isDirty(doc: Document): boolean {
|
||||
this.trackState()
|
||||
return this.dirtyDocuments.has(doc.id)
|
||||
}
|
||||
|
||||
@@ -170,6 +183,7 @@ export class OpenDocumentsService {
|
||||
}
|
||||
|
||||
save() {
|
||||
this.markChanged()
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
OPEN_DOCUMENT_SERVICE.DOCUMENTS,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Injectable, signal } from '@angular/core'
|
||||
import { ObjectWithPermissions } from '../data/object-with-permissions'
|
||||
import { User } from '../data/user'
|
||||
|
||||
@@ -39,16 +39,19 @@ export enum PermissionType {
|
||||
export class PermissionsService {
|
||||
private permissions: string[]
|
||||
private currentUser: User
|
||||
private readonly permissionsVersion = signal(0)
|
||||
|
||||
public initialize(permissions: string[], currentUser: User) {
|
||||
this.permissions = permissions
|
||||
this.currentUser = currentUser
|
||||
this.permissionsVersion.update((version) => version + 1)
|
||||
}
|
||||
|
||||
public currentUserCan(
|
||||
action: PermissionAction,
|
||||
type: PermissionType
|
||||
): boolean {
|
||||
this.permissionsVersion()
|
||||
return (
|
||||
this.currentUser?.is_superuser ||
|
||||
this.permissions?.includes(this.getPermissionCode(action, type))
|
||||
|
||||
@@ -96,7 +96,7 @@ describe(`Additional service tests for SavedViewService`, () => {
|
||||
})
|
||||
|
||||
it('should sort dashboard views', () => {
|
||||
service['savedViews'] = saved_views
|
||||
service['savedViews'].set(saved_views)
|
||||
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
|
||||
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS) return [1, 2, 3]
|
||||
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return [3, 1, 2]
|
||||
@@ -110,7 +110,7 @@ describe(`Additional service tests for SavedViewService`, () => {
|
||||
})
|
||||
|
||||
it('should use user-specific dashboard visibility when configured', () => {
|
||||
service['savedViews'] = saved_views
|
||||
service['savedViews'].set(saved_views)
|
||||
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
|
||||
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS) return [4, 2]
|
||||
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return []
|
||||
@@ -119,7 +119,7 @@ describe(`Additional service tests for SavedViewService`, () => {
|
||||
})
|
||||
|
||||
it('should sort sidebar views', () => {
|
||||
service['savedViews'] = saved_views
|
||||
service['savedViews'].set(saved_views)
|
||||
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
|
||||
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS) return [1, 2, 3]
|
||||
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER) return [3, 1, 2]
|
||||
@@ -133,7 +133,7 @@ describe(`Additional service tests for SavedViewService`, () => {
|
||||
})
|
||||
|
||||
it('should use user-specific sidebar visibility when configured', () => {
|
||||
service['savedViews'] = saved_views
|
||||
service['savedViews'].set(saved_views)
|
||||
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
|
||||
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS) return [4, 2]
|
||||
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER) return []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { inject, Injectable, signal } from '@angular/core'
|
||||
import { combineLatest, Observable, Subject } from 'rxjs'
|
||||
import { takeUntil, tap } from 'rxjs/operators'
|
||||
import { Results } from 'src/app/data/results'
|
||||
@@ -17,8 +17,10 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
|
||||
private settingsService = inject(SettingsService)
|
||||
private documentService = inject(DocumentService)
|
||||
|
||||
private savedViews: SavedView[] = []
|
||||
private savedViewDocumentCounts: Map<number, number> = new Map()
|
||||
private readonly savedViews = signal<SavedView[]>([])
|
||||
private readonly savedViewDocumentCounts = signal<Map<number, number>>(
|
||||
new Map()
|
||||
)
|
||||
private unsubscribeNotifier: Subject<void> = new Subject<void>()
|
||||
|
||||
constructor() {
|
||||
@@ -37,15 +39,16 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
|
||||
tap({
|
||||
next: (r) => {
|
||||
const views = r.results.map((view) => this.withUserVisibility(view))
|
||||
this.savedViews = views
|
||||
this.savedViews.set(views)
|
||||
r.results = views
|
||||
this._loading = false
|
||||
this.settingsService.dashboardIsEmpty =
|
||||
this.settingsService.dashboardIsEmpty.set(
|
||||
this.dashboardViews.length === 0
|
||||
)
|
||||
},
|
||||
error: () => {
|
||||
this._loading = false
|
||||
this.settingsService.dashboardIsEmpty = true
|
||||
this.settingsService.dashboardIsEmpty.set(true)
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -63,8 +66,8 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
get allViews() {
|
||||
return this.savedViews
|
||||
get allViews(): SavedView[] {
|
||||
return this.savedViews()
|
||||
}
|
||||
|
||||
private getVisibleViewIds(setting: string): number[] {
|
||||
@@ -95,7 +98,9 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
|
||||
}
|
||||
|
||||
get sidebarViews(): SavedView[] {
|
||||
const sidebarViews = this.savedViews.filter((v) => this.isSidebarVisible(v))
|
||||
const sidebarViews = this.savedViews().filter((v) =>
|
||||
this.isSidebarVisible(v)
|
||||
)
|
||||
|
||||
const sorted: number[] = this.settingsService.get(
|
||||
SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER
|
||||
@@ -110,7 +115,7 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
|
||||
}
|
||||
|
||||
get dashboardViews(): SavedView[] {
|
||||
const dashboardViews = this.savedViews.filter((v) =>
|
||||
const dashboardViews = this.savedViews().filter((v) =>
|
||||
this.isDashboardVisible(v)
|
||||
)
|
||||
|
||||
@@ -176,10 +181,12 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
|
||||
}
|
||||
|
||||
public setDocumentCount(view: SavedView, count: number) {
|
||||
this.savedViewDocumentCounts.set(view.id, count)
|
||||
const counts = new Map(this.savedViewDocumentCounts())
|
||||
counts.set(view.id, count)
|
||||
this.savedViewDocumentCounts.set(counts)
|
||||
}
|
||||
|
||||
public getDocumentCount(view: SavedView): number {
|
||||
return this.savedViewDocumentCounts.get(view.id)
|
||||
return this.savedViewDocumentCounts().get(view.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
HttpTestingController,
|
||||
provideHttpClientTesting,
|
||||
} from '@angular/common/http/testing'
|
||||
import { fakeAsync, TestBed, tick } from '@angular/core/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
@@ -122,7 +122,8 @@ describe('SettingsService', () => {
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
|
||||
it('should catch error and show toast on retrieve ui_settings error', fakeAsync(() => {
|
||||
it('should catch error and show toast on retrieve ui_settings error', () => {
|
||||
jest.useFakeTimers()
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||
@@ -130,9 +131,10 @@ describe('SettingsService', () => {
|
||||
{ detail: 'You do not have permission to perform this action.' },
|
||||
{ status: 403, statusText: 'Forbidden' }
|
||||
)
|
||||
tick(500)
|
||||
jest.advanceTimersByTime(500)
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
}))
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('calls ui_settings api endpoint with POST on store', () => {
|
||||
let req = httpTestingController.expectOne(
|
||||
@@ -392,22 +394,22 @@ describe('SettingsService', () => {
|
||||
req.flush(ui_settings)
|
||||
settingsService.initializeDisplayFields()
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[0])
|
||||
settingsService.allDisplayFields().includes(DEFAULT_DISPLAY_FIELDS[0])
|
||||
).toBeTruthy() // title
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[4])
|
||||
settingsService.allDisplayFields().includes(DEFAULT_DISPLAY_FIELDS[4])
|
||||
).toBeFalsy() // correspondent
|
||||
|
||||
settingsService.set(SETTINGS_KEYS.NOTES_ENABLED, false)
|
||||
settingsService.initializeDisplayFields()
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[8])
|
||||
settingsService.allDisplayFields().includes(DEFAULT_DISPLAY_FIELDS[8])
|
||||
).toBeFalsy() // notes
|
||||
|
||||
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(true)
|
||||
settingsService.initializeDisplayFields()
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[4])
|
||||
settingsService.allDisplayFields().includes(DEFAULT_DISPLAY_FIELDS[4])
|
||||
).toBeTruthy() // correspondent
|
||||
})
|
||||
|
||||
@@ -422,12 +424,14 @@ describe('SettingsService', () => {
|
||||
)
|
||||
settingsService.initializeDisplayFields()
|
||||
expect(
|
||||
settingsService.allDisplayFields.includes(DEFAULT_DISPLAY_FIELDS[0])
|
||||
settingsService.allDisplayFields().includes(DEFAULT_DISPLAY_FIELDS[0])
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
settingsService.allDisplayFields.find(
|
||||
(f) => f.id === `${DisplayField.CUSTOM_FIELD}${customFields[0].id}`
|
||||
).name
|
||||
settingsService
|
||||
.allDisplayFields()
|
||||
.find(
|
||||
(f) => f.id === `${DisplayField.CUSTOM_FIELD}${customFields[0].id}`
|
||||
).name
|
||||
).toEqual(customFields[0].name)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LOCALE_ID,
|
||||
Renderer2,
|
||||
RendererFactory2,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { Meta } from '@angular/platform-browser'
|
||||
import { CookieService } from 'ngx-cookie-service'
|
||||
@@ -294,7 +295,8 @@ export class SettingsService {
|
||||
protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/'
|
||||
|
||||
private settings: Record<string, any> = {}
|
||||
currentUser: User
|
||||
private readonly settingsVersion = signal(0)
|
||||
readonly currentUser = signal<User>(undefined)
|
||||
|
||||
public settingsSaved: EventEmitter<any> = new EventEmitter()
|
||||
|
||||
@@ -303,17 +305,14 @@ export class SettingsService {
|
||||
return this._renderer
|
||||
}
|
||||
|
||||
public dashboardIsEmpty: boolean = false
|
||||
readonly dashboardIsEmpty = signal(false)
|
||||
readonly globalDropzoneEnabled = signal(true)
|
||||
readonly globalDropzoneActive = signal(false)
|
||||
readonly organizingSidebarSavedViews = signal(false)
|
||||
|
||||
public globalDropzoneEnabled: boolean = true
|
||||
public globalDropzoneActive: boolean = false
|
||||
public organizingSidebarSavedViews: boolean = false
|
||||
|
||||
private _allDisplayFields: Array<{ id: DisplayField; name: string }> =
|
||||
readonly allDisplayFields = signal<Array<{ id: DisplayField; name: string }>>(
|
||||
DEFAULT_DISPLAY_FIELDS
|
||||
public get allDisplayFields(): Array<{ id: DisplayField; name: string }> {
|
||||
return this._allDisplayFields
|
||||
}
|
||||
)
|
||||
public displayFieldsInit: EventEmitter<boolean> = new EventEmitter()
|
||||
|
||||
constructor() {
|
||||
@@ -326,6 +325,10 @@ export class SettingsService {
|
||||
return !UNSAFE_OBJECT_KEYS.has(key)
|
||||
}
|
||||
|
||||
public trackChanges(): void {
|
||||
this.settingsVersion()
|
||||
}
|
||||
|
||||
private assignSafeSettings(source: Record<string, any>) {
|
||||
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
||||
return
|
||||
@@ -363,10 +366,10 @@ export class SettingsService {
|
||||
// to update lang cookie
|
||||
if (this.settings['language']?.length)
|
||||
this.setLanguage(this.settings['language'])
|
||||
this.currentUser = uisettings.user
|
||||
this.currentUser.set(uisettings.user)
|
||||
this.permissionsService.initialize(
|
||||
uisettings.permissions,
|
||||
this.currentUser
|
||||
this.currentUser()
|
||||
)
|
||||
|
||||
this.initializeDisplayFields()
|
||||
@@ -375,44 +378,39 @@ export class SettingsService {
|
||||
}
|
||||
|
||||
public initializeDisplayFields() {
|
||||
this._allDisplayFields = DEFAULT_DISPLAY_FIELDS
|
||||
const displayFields = DEFAULT_DISPLAY_FIELDS?.map((field) => {
|
||||
if (
|
||||
field.id === DisplayField.NOTES &&
|
||||
!this.get(SETTINGS_KEYS.NOTES_ENABLED)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
this._allDisplayFields = this._allDisplayFields
|
||||
?.map((field) => {
|
||||
if (
|
||||
field.id === DisplayField.NOTES &&
|
||||
!this.get(SETTINGS_KEYS.NOTES_ENABLED)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
[
|
||||
DisplayField.TITLE,
|
||||
DisplayField.CREATED,
|
||||
DisplayField.ADDED,
|
||||
DisplayField.ASN,
|
||||
DisplayField.PAGE_COUNT,
|
||||
DisplayField.SHARED,
|
||||
].includes(field.id)
|
||||
) {
|
||||
return field
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
DisplayField.TITLE,
|
||||
DisplayField.CREATED,
|
||||
DisplayField.ADDED,
|
||||
DisplayField.ASN,
|
||||
DisplayField.PAGE_COUNT,
|
||||
DisplayField.SHARED,
|
||||
].includes(field.id)
|
||||
) {
|
||||
return field
|
||||
}
|
||||
let type: PermissionType = Object.values(PermissionType).find((t) =>
|
||||
t.includes(field.id)
|
||||
)
|
||||
if (field.id === DisplayField.OWNER) {
|
||||
type = PermissionType.User
|
||||
}
|
||||
return this.permissionsService.currentUserCan(PermissionAction.View, type)
|
||||
? field
|
||||
: null
|
||||
}).filter(Boolean)
|
||||
|
||||
let type: PermissionType = Object.values(PermissionType).find((t) =>
|
||||
t.includes(field.id)
|
||||
)
|
||||
if (field.id === DisplayField.OWNER) {
|
||||
type = PermissionType.User
|
||||
}
|
||||
return this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
type
|
||||
)
|
||||
? field
|
||||
: null
|
||||
})
|
||||
.filter((f) => f)
|
||||
this.allDisplayFields.set(displayFields)
|
||||
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
@@ -421,13 +419,15 @@ export class SettingsService {
|
||||
)
|
||||
) {
|
||||
this.customFieldsService.listAll().subscribe((r) => {
|
||||
this._allDisplayFields = this._allDisplayFields.concat(
|
||||
r.results.map((field) => {
|
||||
return {
|
||||
id: `${DisplayField.CUSTOM_FIELD}${field.id}` as any,
|
||||
name: field.name,
|
||||
}
|
||||
})
|
||||
this.allDisplayFields.set(
|
||||
displayFields.concat(
|
||||
r.results.map((field) => {
|
||||
return {
|
||||
id: `${DisplayField.CUSTOM_FIELD}${field.id}` as any,
|
||||
name: field.name,
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
this.displayFieldsInit.emit(true)
|
||||
})
|
||||
@@ -438,8 +438,8 @@ export class SettingsService {
|
||||
|
||||
get displayName(): string {
|
||||
return (
|
||||
this.currentUser.first_name ??
|
||||
this.currentUser.username ??
|
||||
this.currentUser()?.first_name ??
|
||||
this.currentUser()?.username ??
|
||||
''
|
||||
).trim()
|
||||
}
|
||||
@@ -572,7 +572,7 @@ export class SettingsService {
|
||||
|
||||
// special case to fallback
|
||||
if (key === SETTINGS_KEYS.DEFAULT_PERMS_OWNER && value === undefined) {
|
||||
return this.currentUser.id
|
||||
return this.currentUser()?.id
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
@@ -606,6 +606,7 @@ export class SettingsService {
|
||||
if (index == keys.length - 1) settingObj[keyPart] = value
|
||||
else settingObj = settingObj[keyPart]
|
||||
})
|
||||
this.settingsVersion.update((version) => version + 1)
|
||||
}
|
||||
|
||||
private settingIsSet(key: string): boolean {
|
||||
@@ -688,7 +689,7 @@ export class SettingsService {
|
||||
}
|
||||
|
||||
offerTour(): boolean {
|
||||
return this.dashboardIsEmpty && !this.get(SETTINGS_KEYS.TOUR_COMPLETE)
|
||||
return this.dashboardIsEmpty() && !this.get(SETTINGS_KEYS.TOUR_COMPLETE)
|
||||
}
|
||||
|
||||
completeTour() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { Injectable, inject, signal } from '@angular/core'
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { first, map, takeUntil, tap } from 'rxjs/operators'
|
||||
import {
|
||||
@@ -23,44 +23,44 @@ export class TasksService {
|
||||
|
||||
public loading: boolean = false
|
||||
|
||||
private fileTasks: PaperlessTask[] = []
|
||||
private readonly fileTasks = signal<PaperlessTask[]>([])
|
||||
|
||||
private unsubscribeNotifer: Subject<any> = new Subject()
|
||||
|
||||
public get total(): number {
|
||||
return this.fileTasks.length
|
||||
return this.fileTasks().length
|
||||
}
|
||||
|
||||
public get allFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.slice(0)
|
||||
return this.fileTasks().slice(0)
|
||||
}
|
||||
|
||||
public get queuedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter(
|
||||
return this.fileTasks().filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Pending
|
||||
)
|
||||
}
|
||||
|
||||
public get startedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter(
|
||||
return this.fileTasks().filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Started
|
||||
)
|
||||
}
|
||||
|
||||
public get completedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter(
|
||||
return this.fileTasks().filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Success
|
||||
)
|
||||
}
|
||||
|
||||
public get failedFileTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter(
|
||||
return this.fileTasks().filter(
|
||||
(t) => t.status === PaperlessTaskStatus.Failure
|
||||
)
|
||||
}
|
||||
|
||||
public get needsAttentionTasks(): PaperlessTask[] {
|
||||
return this.fileTasks.filter((t) =>
|
||||
return this.fileTasks().filter((t) =>
|
||||
[PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked].includes(
|
||||
t.status
|
||||
)
|
||||
@@ -81,7 +81,7 @@ export class TasksService {
|
||||
.pipe(map((r) => r.results))
|
||||
.pipe(takeUntil(this.unsubscribeNotifer), first())
|
||||
.subscribe((r) => {
|
||||
this.fileTasks = r
|
||||
this.fileTasks.set(r)
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export class UploadDocumentsService {
|
||||
let status = this.websocketStatusService.newFileUpload(file.name)
|
||||
|
||||
status.message = $localize`Connecting...`
|
||||
this.websocketStatusService.statusChanged()
|
||||
|
||||
this.uploadSubscriptions[file.name] = this.documentService
|
||||
.uploadDocument(formData)
|
||||
@@ -35,9 +36,11 @@ export class UploadDocumentsService {
|
||||
event.total
|
||||
)
|
||||
status.message = $localize`Uploading...`
|
||||
this.websocketStatusService.statusChanged()
|
||||
} else if (event.type == HttpEventType.Response) {
|
||||
status.taskId = event.body['task_id'] ?? event.body.toString()
|
||||
status.message = $localize`Upload complete, waiting...`
|
||||
this.websocketStatusService.statusChanged()
|
||||
this.uploadSubscriptions[file.name]?.complete()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,11 +45,11 @@ describe('ConsumerStatusService', () => {
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = {
|
||||
settingsService.currentUser.set({
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
}
|
||||
})
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
})
|
||||
@@ -356,12 +356,12 @@ describe('ConsumerStatusService', () => {
|
||||
})
|
||||
|
||||
it('should notify user if user can view or is in group', () => {
|
||||
settingsService.currentUser = {
|
||||
settingsService.currentUser.set({
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
is_superuser: false,
|
||||
groups: [1],
|
||||
}
|
||||
})
|
||||
websocketStatusService.connect()
|
||||
server.send({
|
||||
type: WebsocketStatusType.STATUS_UPDATE,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { Injectable, inject, signal } from '@angular/core'
|
||||
import { Subject } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { User } from '../data/user'
|
||||
@@ -106,7 +106,7 @@ export class WebsocketStatusService {
|
||||
|
||||
private statusWebSocket: WebSocket
|
||||
|
||||
private consumerStatus: FileStatus[] = []
|
||||
private readonly consumerStatus = signal<FileStatus[]>([])
|
||||
|
||||
private readonly documentDetectedSubject = new Subject<FileStatus>()
|
||||
private readonly documentConsumptionFinishedSubject =
|
||||
@@ -119,14 +119,14 @@ export class WebsocketStatusService {
|
||||
|
||||
private get(taskId: string, filename?: string) {
|
||||
let status =
|
||||
this.consumerStatus.find((e) => e.taskId == taskId) ||
|
||||
this.consumerStatus.find(
|
||||
this.consumerStatus().find((e) => e.taskId == taskId) ||
|
||||
this.consumerStatus().find(
|
||||
(e) => e.filename == filename && e.taskId == null
|
||||
)
|
||||
let created = false
|
||||
if (!status) {
|
||||
status = new FileStatus()
|
||||
this.consumerStatus.push(status)
|
||||
this.consumerStatus.update((statuses) => [...statuses, status])
|
||||
created = true
|
||||
}
|
||||
status.taskId = taskId
|
||||
@@ -137,24 +137,30 @@ export class WebsocketStatusService {
|
||||
newFileUpload(filename: string): FileStatus {
|
||||
let status = new FileStatus()
|
||||
status.filename = filename
|
||||
this.consumerStatus.push(status)
|
||||
this.consumerStatus.update((statuses) => [...statuses, status])
|
||||
return status
|
||||
}
|
||||
|
||||
statusChanged() {
|
||||
this.consumerStatus.update((statuses) => [...statuses])
|
||||
}
|
||||
|
||||
getConsumerStatus(phase?: FileStatusPhase) {
|
||||
if (phase != null) {
|
||||
return this.consumerStatus.filter((s) => s.phase == phase)
|
||||
return this.consumerStatus().filter((s) => s.phase == phase)
|
||||
} else {
|
||||
return this.consumerStatus
|
||||
return this.consumerStatus()
|
||||
}
|
||||
}
|
||||
|
||||
getConsumerStatusNotCompleted() {
|
||||
return this.consumerStatus.filter((s) => s.phase < FileStatusPhase.SUCCESS)
|
||||
return this.consumerStatus().filter(
|
||||
(s) => s.phase < FileStatusPhase.SUCCESS
|
||||
)
|
||||
}
|
||||
|
||||
getConsumerStatusCompleted() {
|
||||
return this.consumerStatus.filter(
|
||||
return this.consumerStatus().filter(
|
||||
(s) =>
|
||||
s.phase == FileStatusPhase.FAILED || s.phase == FileStatusPhase.SUCCESS
|
||||
)
|
||||
@@ -211,7 +217,7 @@ export class WebsocketStatusService {
|
||||
groups_can_view?: number[]
|
||||
}): boolean {
|
||||
// see paperless.consumers.StatusConsumer._can_view
|
||||
const user: User = this.settingsService.currentUser
|
||||
const user: User = this.settingsService.currentUser()
|
||||
return (
|
||||
!messageData.owner_id ||
|
||||
user.is_superuser ||
|
||||
@@ -250,6 +256,7 @@ export class WebsocketStatusService {
|
||||
if (messageData.status in FileStatusPhase) {
|
||||
status.phase = FileStatusPhase[messageData.status]
|
||||
}
|
||||
this.statusChanged()
|
||||
|
||||
switch (status.phase) {
|
||||
case FileStatusPhase.STARTED:
|
||||
@@ -281,6 +288,7 @@ export class WebsocketStatusService {
|
||||
fail(status: FileStatus, message: string) {
|
||||
status.message = message
|
||||
status.phase = FileStatusPhase.FAILED
|
||||
this.statusChanged()
|
||||
this.documentConsumptionFailedSubject.next(status)
|
||||
}
|
||||
|
||||
@@ -294,24 +302,28 @@ export class WebsocketStatusService {
|
||||
dismiss(status: FileStatus) {
|
||||
let index
|
||||
if (status.taskId != null) {
|
||||
index = this.consumerStatus.findIndex((s) => s.taskId == status.taskId)
|
||||
index = this.consumerStatus().findIndex((s) => s.taskId == status.taskId)
|
||||
} else {
|
||||
index = this.consumerStatus.findIndex(
|
||||
index = this.consumerStatus().findIndex(
|
||||
(s) => s.filename == status.filename
|
||||
)
|
||||
}
|
||||
|
||||
if (index > -1) {
|
||||
this.consumerStatus.splice(index, 1)
|
||||
this.consumerStatus.update((statuses) =>
|
||||
statuses.filter((_, statusIndex) => statusIndex !== index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dismissCompleted() {
|
||||
this.consumerStatus = this.consumerStatus.filter(
|
||||
(status) =>
|
||||
![FileStatusPhase.SUCCESS, FileStatusPhase.FAILED].includes(
|
||||
status.phase
|
||||
)
|
||||
this.consumerStatus.update((statuses) =>
|
||||
statuses.filter(
|
||||
(status) =>
|
||||
![FileStatusPhase.SUCCESS, FileStatusPhase.FAILED].includes(
|
||||
status.phase
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user