Chorehancement: update to Angular v22, 'zoneless' / 'reactive' (#13114)

This commit is contained in:
shamoon
2026-07-10 00:42:16 -07:00
committed by GitHub
parent f244442c65
commit 106b41a15c
213 changed files with 5363 additions and 5842 deletions
@@ -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)
+1 -1
View File
@@ -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)
})
})
+59 -58
View File
@@ -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() {
+10 -10
View File
@@ -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
)
)
)
}