Direct signals in lots more components

This commit is contained in:
shamoon
2026-07-08 16:43:13 -07:00
parent 08871964c3
commit b84b877773
46 changed files with 245 additions and 477 deletions
@@ -97,11 +97,11 @@ export class ConfigComponent
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (config) => {
this.loading = false
this.loading.set(false)
this.initialize(config)
},
error: (e) => {
this.loading = false
this.loading.set(false)
this.toastService.showError($localize`Error retrieving config`, e)
},
})
@@ -162,20 +162,20 @@ export class ConfigComponent
}
public saveConfig() {
this.loading = true
this.loading.set(true)
this.configService
.saveConfig(this.configForm.value as PaperlessConfig)
.pipe(takeUntil(this.unsubscribeNotifier), first())
.subscribe({
next: (config) => {
this.loading = false
this.loading.set(false)
this.initialize(config)
this.store.next(config)
this.settingsService.initializeSettings().subscribe()
this.toastService.showInfo($localize`Configuration updated`)
},
error: (e) => {
this.loading = false
this.loading.set(false)
this.toastService.showError(
$localize`An error occurred updating configuration`,
e
@@ -189,20 +189,20 @@ export class ConfigComponent
}
public uploadFile(file: File, key: string) {
this.loading = true
this.loading.set(true)
this.configService
.uploadFile(file, this.configForm.value['id'], key)
.pipe(takeUntil(this.unsubscribeNotifier), first())
.subscribe({
next: (config) => {
this.loading = false
this.loading.set(false)
this.initialize(config)
this.store.next(config)
this.settingsService.initializeSettings().subscribe()
this.toastService.showInfo($localize`File successfully updated`)
},
error: (e) => {
this.loading = false
this.loading.set(false)
this.toastService.showError(
$localize`An error occurred uploading file`,
e
@@ -61,7 +61,7 @@ export class LogsComponent
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((result) => {
this.logFiles.set(result)
this.loading = false
this.loading.set(false)
if (this.logFiles().length > 0) {
this.activeLog.set(this.logFiles()[0])
this.reloadLogs()
@@ -86,14 +86,14 @@ export class LogsComponent
}
reloadLogs() {
this.loading = true
this.loading.set(true)
const shouldStickToBottom = this.isNearBottom()
this.logService
.get(this.activeLog(), this.limit())
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
this.loading = false
this.loading.set(false)
const parsed = this.parseLogsWithLevel(result)
const hasChanges =
parsed.length !== this.logs().length ||
@@ -115,7 +115,7 @@ export class LogsComponent
},
error: () => {
this.logs.set([])
this.loading = false
this.loading.set(false)
},
})
}
@@ -161,7 +161,7 @@ describe('SettingsComponent', () => {
viewportScroller = TestBed.inject(ViewportScroller)
toastService = TestBed.inject(ToastService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = users[0]
settingsService.currentUser.set(users[0])
userService = TestBed.inject(UserService)
permissionsService = TestBed.inject(PermissionsService)
modalService = TestBed.inject(NgbModal)
@@ -732,7 +732,7 @@ export class TasksComponent
this.reloadSectionCounts()
this.loading = true
this.loading.set(true)
this.tasksService
.list(
this.page,
@@ -748,7 +748,7 @@ export class TasksComponent
if (this.selectedSection !== TaskSection.All) {
this.setSectionCount(this.selectedSection, result.count)
}
this.loading = false
this.loading.set(false)
if (
this.page > 1 &&
this.pagedTasks().length === 0 &&
@@ -759,7 +759,7 @@ export class TasksComponent
}
},
error: () => {
this.loading = false
this.loading.set(false)
},
})
}
@@ -54,7 +54,7 @@ export class TrashComponent
}
reload() {
this.loading = true
this.loading.set(true)
this.trashService
.getTrash(this.page())
.pipe(
@@ -62,11 +62,11 @@ export class TrashComponent
this.documentsInTrash.set(r.results)
this.totalDocuments.set(r.count)
this.selectedDocuments.set(new Set())
this.loading = false
this.loading.set(false)
})
)
.subscribe(() => {
this.show = true
this.show.set(true)
})
}
@@ -109,7 +109,7 @@ describe('UsersAndGroupsComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toHaveBeenCalled()
settingsService.currentUser = users[1] // simulate logged in as different user
settingsService.currentUser.set(users[1]) // simulate logged in as different user
editDialog.succeeded.emit(users[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved user "${users[0].username}".`
@@ -148,7 +148,7 @@ describe('UsersAndGroupsComponent', () => {
.mockImplementation(() => {})
const editDialog = modal.componentInstance as UserEditDialogComponent
editDialog.passwordIsSet = true
settingsService.currentUser = users[0] // simulate logged in as same user
settingsService.currentUser.set(users[0]) // simulate logged in as same user
editDialog.succeeded.emit(users[0])
fixture.detectChanges()
tick(2600)
@@ -109,7 +109,7 @@ export class UsersAndGroupsComponent
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newUser: User) => {
if (
newUser.id === this.settings.currentUser.id &&
newUser.id === this.settings.currentUser().id &&
(modal.componentInstance as UserEditDialogComponent).passwordIsSet
) {
this.toastService.showInfo(
@@ -1,7 +1,7 @@
<nav class="navbar navbar-dark fixed-top bg-primary flex-md-nowrap p-0 shadow-sm">
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
(click)="mobileSearchHidden = false; isMenuCollapsed = !isMenuCollapsed">
(click)="closeMobileSearch(); toggleMenuCollapsed()">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand d-flex align-items-center me-0 px-3 py-3 order-sm-0"
@@ -23,7 +23,7 @@
</div>
</a>
<div class="search-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
[class.mobile-hidden]="mobileSearchHidden">
[class.mobile-hidden]="mobileSearchHidden()">
<div class="col-12 col-md-7">
<pngx-global-search></pngx-global-search>
</div>
@@ -68,8 +68,8 @@
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse"
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
[ngbCollapse]="isMenuCollapsed">
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating()"
[ngbCollapse]="isMenuCollapsed()">
@if (canSaveSettings) {
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
@if (slimSidebarEnabled) {
@@ -104,7 +104,7 @@
</h6>
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
@for (view of savedViewService.sidebarViews; track view.id) {
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews()"
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($event)">
<a class="nav-link" routerLink="view/{{view.id}}"
@@ -120,7 +120,7 @@
<span class="badge bg-info text-dark position-absolute top-0 end-0 d-none d-md-block">{{ savedViewService.getDocumentCount(view) }}</span>
}
</a>
@if (settingsService.organizingSidebarSavedViews) {
@if (settingsService.organizingSidebarSavedViews()) {
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
<i-bs name="grip-vertical"></i-bs>
</div>
@@ -329,10 +329,10 @@
{{ versionString }}
</a>
</div>
@if (!settingsService.updateCheckingIsSet || appRemoteVersion) {
@if (!settingsService.updateCheckingIsSet || appRemoteVersion()) {
<div class="version-check">
<ng-template #updateAvailablePopContent>
<span class="small">Paperless-ngx {{ appRemoteVersion.version }} <ng-container i18n>is
<span class="small">Paperless-ngx {{ appRemoteVersion().version }} <ng-container i18n>is
available.</ng-container><br /><ng-container i18n>Click to view.</ng-container></span>
</ng-template>
<ng-template #updateCheckingNotEnabledPopContent>
@@ -350,13 +350,13 @@
</p>
</ng-template>
@if (settingsService.updateCheckingIsSet) {
@if (appRemoteVersion.update_available) {
@if (appRemoteVersion().update_available) {
<a class="small text-decoration-none" target="_blank" rel="noopener noreferrer"
href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave"
container="body">
<i-bs width="1.2em" height="1.2em" name="info-circle" class="me-1"></i-bs>
@if (appRemoteVersion?.update_available) {
@if (appRemoteVersion()?.update_available) {
<ng-container i18n>Update available</ng-container>
}
</a>
@@ -377,7 +377,7 @@
</div>
</nav>
<main role="main" class="ms-sm-auto px-md-4" [class.mobile-search-hidden]="mobileSearchHidden"
<main role="main" class="ms-sm-auto px-md-4" [class.mobile-search-hidden]="mobileSearchHidden()"
[ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10 col-xxxl-11'">
<router-outlet></router-outlet>
</main>
@@ -90,48 +90,12 @@ export class AppFrameComponent
permissionsService = inject(PermissionsService)
private djangoMessagesService = inject(DjangoMessagesService)
private appRemoteVersionSignal = signal<AppRemoteVersion>(null)
private isMenuCollapsedSignal = signal(true)
private slimSidebarAnimatingSignal = signal(false)
private mobileSearchHiddenSignal = signal(false)
readonly appRemoteVersion = signal<AppRemoteVersion>(null)
readonly isMenuCollapsed = signal(true)
readonly slimSidebarAnimating = signal(false)
readonly mobileSearchHidden = signal(false)
private lastScrollY: number = 0
get appRemoteVersion(): AppRemoteVersion {
return this.appRemoteVersionSignal()
}
set appRemoteVersion(value: AppRemoteVersion) {
this.appRemoteVersionSignal.set(value)
}
get isMenuCollapsed(): boolean {
return this.isMenuCollapsedSignal()
}
set isMenuCollapsed(value: boolean) {
this.isMenuCollapsedSignal.set(value)
}
get slimSidebarAnimating(): boolean {
return this.slimSidebarAnimatingSignal()
}
set slimSidebarAnimating(value: boolean) {
this.slimSidebarAnimatingSignal.set(value)
}
get mobileSearchHidden(): boolean {
return this.mobileSearchHiddenSignal()
}
set mobileSearchHidden(value: boolean) {
this.mobileSearchHiddenSignal.set(value)
}
constructor() {
super()
const permissionsService = this.permissionsService
@@ -179,7 +143,7 @@ export class AppFrameComponent
}
toggleSlimSidebar(): void {
this.slimSidebarAnimating = true
this.slimSidebarAnimating.set(true)
const slimSidebarEnabled = !this.slimSidebarEnabled
this.settingsService.set(SETTINGS_KEYS.SLIM_SIDEBAR, slimSidebarEnabled)
if (slimSidebarEnabled) {
@@ -199,7 +163,7 @@ export class AppFrameComponent
},
})
setTimeout(() => {
this.slimSidebarAnimating = false
this.slimSidebarAnimating.set(false)
}, 200) // slightly longer than css animation for slim sidebar
}
@@ -209,6 +173,18 @@ export class AppFrameComponent
this.attributesSectionsCollapsed = !this.attributesSectionsCollapsed
}
toggleMenuCollapsed(): void {
this.isMenuCollapsed.set(!this.isMenuCollapsed())
}
closeMobileSearch(): void {
this.mobileSearchHidden.set(false)
}
setMobileSearchHidden(hidden: boolean): void {
this.mobileSearchHidden.set(hidden)
}
get versionString(): string {
this.settingsService.trackChanges()
return `${environment.appTitle} v${this.settingsService.get(SETTINGS_KEYS.VERSION)}${environment.tag === 'prod' ? '' : ` #${environment.tag}`}`
@@ -311,7 +287,7 @@ export class AppFrameComponent
@HostListener('window:resize')
onWindowResize(): void {
if (!this.isMobileViewport()) {
this.mobileSearchHidden = false
this.mobileSearchHidden.set(false)
}
}
@@ -319,8 +295,8 @@ export class AppFrameComponent
onWindowScroll(): void {
const currentScrollY = window.scrollY
if (!this.isMobileViewport() || this.isMenuCollapsed === false) {
this.mobileSearchHidden = false
if (!this.isMobileViewport() || this.isMenuCollapsed() === false) {
this.mobileSearchHidden.set(false)
this.lastScrollY = currentScrollY
return
}
@@ -328,9 +304,9 @@ export class AppFrameComponent
const delta = currentScrollY - this.lastScrollY
if (currentScrollY <= 0 || delta < -SCROLL_THRESHOLD) {
this.mobileSearchHidden = false
this.mobileSearchHidden.set(false)
} else if (currentScrollY > SCROLL_THRESHOLD && delta > SCROLL_THRESHOLD) {
this.mobileSearchHidden = true
this.mobileSearchHidden.set(true)
}
this.lastScrollY = currentScrollY
@@ -341,7 +317,7 @@ export class AppFrameComponent
}
closeMenu() {
this.isMenuCollapsed = true
this.isMenuCollapsed.set(true)
}
editProfile() {
@@ -404,11 +380,11 @@ export class AppFrameComponent
}
onDragStart(event: CdkDragStart) {
this.settingsService.globalDropzoneEnabled = false
this.settingsService.globalDropzoneEnabled.set(false)
}
onDragEnd(event: CdkDragEnd) {
this.settingsService.globalDropzoneEnabled = true
this.settingsService.globalDropzoneEnabled.set(true)
}
onDrop(event: CdkDragDrop<SavedView[]>) {
@@ -429,7 +405,7 @@ export class AppFrameComponent
this.remoteVersionService
.checkForUpdates()
.subscribe((appRemoteVersion: AppRemoteVersion) => {
this.appRemoteVersion = appRemoteVersion
this.appRemoteVersion.set(appRemoteVersion)
})
}
@@ -459,7 +435,7 @@ export class AppFrameComponent
this.settingsService.trackChanges()
return (
this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT) &&
!this.settingsService.organizingSidebarSavedViews
!this.settingsService.organizingSidebarSavedViews()
)
}
}
@@ -74,7 +74,7 @@ describe('CustomFieldsDropdownComponent', () => {
})
)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 1, username: 'test' }
settingsService.currentUser.set({ id: 1, username: 'test' })
fixture = TestBed.createComponent(CustomFieldsDropdownComponent)
component = fixture.componentInstance
fixture.detectChanges()
@@ -41,7 +41,7 @@ describe('CorrespondentEditDialogComponent', () => {
fixture = TestBed.createComponent(CorrespondentEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -44,7 +44,7 @@ describe('CustomFieldEditDialogComponent', () => {
fixture = TestBed.createComponent(CustomFieldEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -41,7 +41,7 @@ describe('DocumentTypeEditDialogComponent', () => {
fixture = TestBed.createComponent(DocumentTypeEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -45,7 +45,7 @@ describe('GroupEditDialogComponent', () => {
fixture = TestBed.createComponent(GroupEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -58,7 +58,7 @@ describe('MailAccountEditDialogComponent', () => {
fixture = TestBed.createComponent(MailAccountEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -75,7 +75,7 @@ describe('MailRuleEditDialogComponent', () => {
fixture = TestBed.createComponent(MailRuleEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -30,7 +30,7 @@ describe('StoragePathEditDialogComponent', () => {
documentService = TestBed.inject(DocumentService)
fixture = TestBed.createComponent(StoragePathEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -48,7 +48,7 @@ describe('TagEditDialogComponent', () => {
fixture = TestBed.createComponent(TagEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -73,7 +73,7 @@ describe('UserEditDialogComponent', () => {
fixture = TestBed.createComponent(UserEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
permissionsService = TestBed.inject(PermissionsService)
toastService = TestBed.inject(ToastService)
component = fixture.componentInstance
@@ -187,7 +187,7 @@ describe('WorkflowEditDialogComponent', () => {
fixture = TestBed.createComponent(WorkflowEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
settingsService.currentUser.set({ id: 99, username: 'user99' })
component = fixture.componentInstance
fixture.detectChanges()
@@ -136,7 +136,7 @@ describe('TagsComponent', () => {
})
it('should support create new using last search term and open a modal', () => {
settingsService.currentUser = { id: 1 }
settingsService.currentUser.set({ id: 1 })
let activeInstances: NgbModalRef[]
modalService.activeInstances.subscribe((v) => (activeInstances = v))
component.select.filter('foobar')
@@ -117,12 +117,12 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions
if (this.selectionModel.ownerFilter === OwnerFilterType.SELF) {
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = []
this.selectionModel.userID = this.settingsService.currentUser.id
this.selectionModel.userID = this.settingsService.currentUser().id
this.selectionModel.hideUnowned = false
} else if (this.selectionModel.ownerFilter === OwnerFilterType.NOT_SELF) {
this.selectionModel.userID = null
this.selectionModel.includeUsers = []
this.selectionModel.excludeUsers = [this.settingsService.currentUser.id]
this.selectionModel.excludeUsers = [this.settingsService.currentUser().id]
this.selectionModel.hideUnowned = false
} else if (this.selectionModel.ownerFilter === OwnerFilterType.NONE) {
this.selectionModel.userID = null
@@ -7,38 +7,38 @@
<div class="modal-body">
<div class="row">
<div class="col-12 col-md-6">
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text>
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error()?.email"></pngx-input-text>
<div ngbAccordion>
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent">
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm()" class="border-0 bg-transparent">
<div ngbAccordionCollapse>
<div ngbAccordionBody class="p-0 pb-3">
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error?.email_confirm"></pngx-input-text>
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" autocomplete="email" [error]="error()?.email_confirm"></pngx-input-text>
</div>
</div>
</div>
</div>
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error?.password"></pngx-input-password>
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [showReveal]="true" autocomplete="current-password" [error]="error()?.password"></pngx-input-password>
<div ngbAccordion>
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent">
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm()" class="border-0 bg-transparent">
<div ngbAccordionCollapse>
<div ngbAccordionBody class="p-0 pb-3">
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error?.password_confirm"></pngx-input-password>
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" autocomplete="new-password" [error]="error()?.password_confirm"></pngx-input-password>
</div>
</div>
</div>
</div>
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error()?.first_name"></pngx-input-text>
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error()?.first_name"></pngx-input-text>
<div class="mb-3">
<label class="form-label" i18n>API Auth Token</label>
<div class="position-relative">
<div class="input-group">
<input type="text" class="form-control" formControlName="auth_token" readonly>
<button type="button" class="btn btn-outline-secondary" (click)="copyAuthToken()" i18n-title title="Copy">
@if (!copied) {
@if (!copied()) {
<i-bs width="1em" height="1em" name="clipboard-fill"></i-bs>
}
@if (copied) {
@if (copied()) {
<i-bs width="1em" height="1em" name="clipboard-check-fill"></i-bs>
}
<span class="visually-hidden" i18n>Copy</span>
@@ -51,7 +51,7 @@
(confirm)="generateAuthToken()">
</pngx-confirm-button>
</div>
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied" i18n>Copied!</span>
<span class="badge copied-badge bg-primary small fade ms-4 position-absolute top-50 translate-middle-y pe-none z-3" [class.show]="copied()" i18n>Copied!</span>
</div>
<div class="form-text text-muted text-end fst-italic" i18n>Warning: changing the token cannot be undone</div>
</div>
@@ -155,10 +155,10 @@
}
</ul>
<button type="button" class="btn btn-sm btn-outline-secondary ms-2" (click)="copyRecoveryCodes()" i18n-title title="Copy">
@if (!codesCopied) {
@if (!codesCopied()) {
<i-bs width="1em" height="1em" name="clipboard-fill" class="me-1"></i-bs><span i18n>Copy codes</span>
}
@if (codesCopied) {
@if (codesCopied()) {
<i-bs width="1em" height="1em" name="clipboard-check-fill" class="text-primary me-1"></i-bs><span class="text-primary" i18n>Copied!</span>
}
</button>
@@ -50,28 +50,12 @@ export class ProfileEditDialogComponent
private toastService = inject(ToastService)
private clipboard = inject(Clipboard)
private networkActiveSignal = signal(false)
private errorSignal = signal<any>(undefined)
private showPasswordConfirmSignal = signal(false)
private showEmailConfirmSignal = signal(false)
private copiedSignal = signal(false)
private codesCopiedSignal = signal(false)
public get networkActive(): boolean {
return this.networkActiveSignal()
}
public set networkActive(networkActive: boolean) {
this.networkActiveSignal.set(networkActive)
}
public get error(): any {
return this.errorSignal()
}
public set error(error: any) {
this.errorSignal.set(error)
}
readonly networkActive = signal(false)
readonly error = signal<any>(undefined)
readonly showPasswordConfirm = signal(false)
readonly showEmailConfirm = signal(false)
readonly copied = signal(false)
readonly codesCopied = signal(false)
public form = new FormGroup({
email: new FormControl(''),
@@ -87,49 +71,18 @@ export class ProfileEditDialogComponent
private currentPassword: string
private newPassword: string
private passwordConfirm: string
public get showPasswordConfirm(): boolean {
return this.showPasswordConfirmSignal()
}
public set showPasswordConfirm(showPasswordConfirm: boolean) {
this.showPasswordConfirmSignal.set(showPasswordConfirm)
}
public hasUsablePassword: boolean = false
private currentEmail: string
private newEmail: string
private emailConfirm: string
public get showEmailConfirm(): boolean {
return this.showEmailConfirmSignal()
}
public set showEmailConfirm(showEmailConfirm: boolean) {
this.showEmailConfirmSignal.set(showEmailConfirm)
}
public isTotpEnabled: boolean = false
public totpSettings: TotpSettings
public totpSettingsLoading: boolean = false
public totpLoading: boolean = false
public recoveryCodes: string[]
public get copied(): boolean {
return this.copiedSignal()
}
public set copied(copied: boolean) {
this.copiedSignal.set(copied)
}
public get codesCopied(): boolean {
return this.codesCopiedSignal()
}
public set codesCopied(codesCopied: boolean) {
this.codesCopiedSignal.set(codesCopied)
}
public socialAccounts: SocialAccount[] = []
public socialAccountProviders: SocialAccountProvider[] = []
@@ -141,12 +94,12 @@ export class ProfileEditDialogComponent
}
ngOnInit(): void {
this.networkActive = true
this.networkActive.set(true)
this.profileService
.get()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((profile) => {
this.networkActive = false
this.networkActive.set(false)
this.form.patchValue(profile)
this.currentEmail = profile.email
this.form.get('email').valueChanges.subscribe((newEmail) => {
@@ -186,7 +139,7 @@ export class ProfileEditDialogComponent
}
onEmailChange(): void {
this.showEmailConfirm = this.currentEmail !== this.newEmail
this.showEmailConfirm.set(this.currentEmail !== this.newEmail)
if (this.showEmailConfirm) {
this.form.get('email_confirm').enable()
if (this.newEmail !== this.emailConfirm) {
@@ -212,7 +165,7 @@ export class ProfileEditDialogComponent
}
onPasswordChange(): void {
this.showPasswordConfirm = this.currentPassword !== this.newPassword
this.showPasswordConfirm.set(this.currentPassword !== this.newPassword)
if (this.showPasswordConfirm) {
this.form.get('password_confirm').enable()
@@ -228,17 +181,17 @@ export class ProfileEditDialogComponent
}
private setFieldError(fieldName: string, message: string): void {
this.error = {
...(this.error ?? {}),
this.error.set({
...(this.error() ?? {}),
[fieldName]: message,
}
})
}
private clearFieldError(fieldName: string): void {
if (!this.error) return
const error = { ...this.error }
if (!this.error()) return
const error = { ...this.error() }
delete error[fieldName]
this.error = Object.keys(error).length ? error : undefined
this.error.set(Object.keys(error).length ? error : undefined)
}
save(): void {
@@ -57,40 +57,13 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
public SystemStatusItemStatus = SystemStatusItemStatus
public PaperlessTaskType = PaperlessTaskType
private statusSignal = signal<SystemStatus>(undefined)
@Input() status = signal<SystemStatus>(undefined)
public frontendVersion: string = environment.version
private versionMismatchSignal = signal(false)
private copiedSignal = signal(false)
private runningTasksSignal = signal<Set<PaperlessTaskType>>(new Set())
readonly versionMismatch = signal(false)
readonly copied = signal(false)
readonly runningTasks = signal<Set<PaperlessTaskType>>(new Set())
private unsubscribeNotifier: Subject<any> = new Subject()
@Input()
get status(): SystemStatus {
return this.statusSignal()
}
set status(status: SystemStatus) {
this.statusSignal.set(status)
}
get versionMismatch(): boolean {
return this.versionMismatchSignal()
}
set versionMismatch(versionMismatch: boolean) {
this.versionMismatchSignal.set(versionMismatch)
}
get copied(): boolean {
return this.copiedSignal()
}
set copied(copied: boolean) {
this.copiedSignal.set(copied)
}
get currentUserIsSuperUser(): boolean {
return this.permissionsService.isSuperUser()
}
@@ -100,17 +73,18 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
}
public ngOnInit() {
const status = this.status
this.versionMismatch =
const status = this.status()
this.versionMismatch.set(
environment.production &&
status.pngx_version &&
this.frontendVersion &&
status.pngx_version !== this.frontendVersion
if (this.versionMismatch) {
this.status = {
status.pngx_version &&
this.frontendVersion &&
status.pngx_version !== this.frontendVersion
)
if (this.versionMismatch()) {
this.status.set({
...status,
pngx_version: `${status.pngx_version} (frontend: ${this.frontendVersion})`,
}
})
}
this.updateWebsocketStatus(this.websocketStatusService.isConnected())
this.websocketStatusService
@@ -124,10 +98,10 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
}
public copy() {
this.clipboard.copy(JSON.stringify(this.status, null, 4))
this.copied = true
this.clipboard.copy(JSON.stringify(this.status(), null, 4))
this.copied.set(true)
setTimeout(() => {
this.copied = false
this.copied.set(false)
}, 3000)
}
@@ -138,7 +112,7 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
}
public isRunning(taskName: PaperlessTaskType): boolean {
return this.runningTasksSignal().has(taskName)
return this.runningTasks().has(taskName)
}
public runTask(taskName: PaperlessTaskType) {
@@ -148,11 +122,11 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
next: () => {
this.setTaskRunning(taskName, false)
this.systemStatusService.get().subscribe({
next: (status) => {
this.status = {
...this.status,
...status,
}
next: (statusUpdate) => {
this.status.set({
...this.status(),
...statusUpdate,
})
},
})
},
@@ -167,22 +141,22 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
}
private updateWebsocketStatus(connected: boolean): void {
this.status = {
...this.status,
this.status.set({
...this.status(),
websocket_connected: connected
? SystemStatusItemStatus.OK
: SystemStatusItemStatus.ERROR,
}
})
}
private setTaskRunning(taskName: PaperlessTaskType, running: boolean): void {
const runningTasks = new Set(this.runningTasksSignal())
const runningTasks = new Set(this.runningTasks())
if (running) {
runningTasks.add(taskName)
} else {
runningTasks.delete(taskName)
}
this.runningTasksSignal.set(runningTasks)
this.runningTasks.set(runningTasks)
}
ngOnDestroy(): void {
@@ -120,10 +120,10 @@ describe('DashboardComponent', () => {
}).compileComponents()
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = {
settingsService.currentUser.set({
first_name: 'Foo',
last_name: 'Bar',
}
})
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return [0, 2, 3]
})
@@ -137,9 +137,9 @@ describe('DashboardComponent', () => {
it('should show a welcome message', () => {
expect(component.subtitle).toEqual(`Hello Foo, welcome to Paperless-ngx`)
settingsService.currentUser = {
settingsService.currentUser.set({
id: 1,
}
})
expect(component.subtitle).toEqual(`Welcome to Paperless-ngx`)
})
@@ -5,21 +5,21 @@
[draggable]="savedView"
>
@if (count !== null) {
<span title-badge class="badge bg-info text-dark ms-2">{{count}}</span>
@if (count() !== null) {
<span title-badge class="badge bg-info text-dark ms-2">{{count()}}</span>
}
@if (documents.length) {
@if (documents().length) {
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
}
<div content class="wrapper fade" [class.show]="show">
@if (displayMode === DisplayMode.TABLE) {
@if (displayMode() === DisplayMode.TABLE) {
<table class="table table-hover mb-0 mt-n2 align-middle">
<thead>
<tr>
@for (field of displayFields; track field; let i = $index) {
@if (displayFields.includes(field)) {
@for (field of displayFields(); track field; let i = $index) {
@if (displayFields().includes(field)) {
<th
scope="col"
[ngClass]="{
@@ -46,13 +46,13 @@
</tr>
}
}
@for (doc of documents; track doc.id; let i = $index) {
@for (doc of documents(); track doc.id; let i = $index) {
<tr>
@for (field of displayFields; track field; let j = $index) {
@for (field of displayFields(); track field; let j = $index) {
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': j > 1 }">
@if (loading && !documents.length) {
@if (loading && !documents().length) {
<div class="placeholder-glow text-start">
<span class="placeholder bg-secondary w-50" [ngStyle]="{ opacity: 1 - (i * 1/documents.length) }"></span>
<span class="placeholder bg-secondary w-50" [ngStyle]="{ opacity: 1 - (i * 1/documents().length) }\"></span>
</div>
} @else {
@switch (field) {
@@ -112,7 +112,7 @@
@if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
<pngx-custom-field-display [document]="doc" [fieldDisplayKey]="field"></pngx-custom-field-display>
}
@if (j === displayFields.length - 1) {
@if (j === displayFields().length - 1) {
<div class="btn-group position-absolute top-50 end-0 translate-middle-y" (mouseleave)="popupPreview.close()">
<pngx-preview-popup [document]="doc" linkClasses="btn px-4 btn-dark border-dark-subtle" #popupPreview>
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
@@ -129,7 +129,7 @@
}
</tbody>
</table>
} @else if (displayMode === DisplayMode.SMALL_CARDS) {
} @else if (displayMode() === DisplayMode.SMALL_CARDS) {
<div class="row row-cols-paperless-cards my-n2">
@if (loading && !documents.length) {
@for (row of placeholderRows; track row) {
@@ -107,7 +107,7 @@ export class SavedViewWidgetComponent
@Input()
savedView: SavedView
private documentsSignal = signal<Document[]>([])
readonly documents = signal<Document[]>([])
unsubscribeNotifier: Subject<any> = new Subject()
@@ -117,48 +117,16 @@ export class SavedViewWidgetComponent
mouseOnPreview = false
popoverHidden = true
private displayModeSignal = signal<DisplayMode>(null)
readonly displayMode = signal<DisplayMode>(null)
private displayFieldsSignal = signal<DisplayField[]>(
readonly displayFields = signal<DisplayField[]>(
DEFAULT_DASHBOARD_DISPLAY_FIELDS
)
private countSignal = signal<number>(null)
readonly count = signal<number>(null)
placeholderRows: number[] = []
get documents(): Document[] {
return this.documentsSignal()
}
set documents(value: Document[]) {
this.documentsSignal.set(value)
}
get displayMode(): DisplayMode {
return this.displayModeSignal()
}
set displayMode(value: DisplayMode) {
this.displayModeSignal.set(value)
}
get displayFields(): DisplayField[] {
return this.displayFieldsSignal()
}
set displayFields(value: DisplayField[]) {
this.displayFieldsSignal.set(value)
}
get count(): number {
return this.countSignal()
}
set count(value: number) {
this.countSignal.set(value)
}
ngOnInit(): void {
this.placeholderRows = Array.from(
{
@@ -166,7 +134,7 @@ export class SavedViewWidgetComponent
},
(_, index) => index
)
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
this.displayMode.set(this.savedView.display_mode ?? DisplayMode.TABLE)
if (
this.permissionsService.currentUserCan(
@@ -192,7 +160,7 @@ export class SavedViewWidgetComponent
// filter by perms etc
this.displayFields = this.displayFields.filter(
(field) =>
this.settingsService.allDisplayFields.find((f) => f.id === field) !==
this.settingsService.allDisplayFields().find((f) => f.id === field) !==
undefined
)
@@ -224,8 +192,8 @@ export class SavedViewWidgetComponent
)
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((result) => {
this.documents = result.results
this.count = result.count
this.documents.set(result.results)
this.count.set(result.count)
this.savedViewService.setDocumentCount(this.savedView, result.count)
this.loading = false
this.show = true
@@ -286,7 +286,7 @@ describe('DocumentDetailComponent', () => {
toastService = TestBed.inject(ToastService)
documentListViewService = TestBed.inject(DocumentListViewService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 1 }
settingsService.currentUser.set({ id: 1 })
customFieldsService = TestBed.inject(CustomFieldsService)
deviceDetectorService = TestBed.inject(DeviceDetectorService)
fixture = TestBed.createComponent(DocumentDetailComponent)
@@ -134,7 +134,7 @@
<small i18n>{document.page_count, plural, =1 {1 page} other {{{document.page_count}} pages}}</small>
</div>
}
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser().id) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username | async}}</small>
</div>
@@ -113,7 +113,7 @@
<small>#{{document.archive_serial_number}}</small>
</div>
}
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser().id) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
<small>{{document.owner | username | async}}</small>
@@ -36,7 +36,7 @@
</button>
<div ngbDropdownMenu aria-labelledby="dropdownDisplayFields" class="shadow">
<div class="px-3">
@for (field of settingsService.allDisplayFields; track field.id) {
@for (field of settingsService.allDisplayFields(); track field.id) {
<div class="form-check my-1">
<input class="form-check-input mt-1" type="checkbox" id="displayField{{field.id}}" [checked]="activeDisplayFields.includes(field.id)" (change)="toggleDisplayField(field.id)">
<label class="form-check-label" for="displayField{{field.id}}">{{field.name}}</label>
@@ -837,11 +837,10 @@ describe('DocumentListComponent', () => {
it('should get custom field title', () => {
fixture.detectChanges()
jest
.spyOn(settingsService, 'allDisplayFields', 'get')
.mockReturnValue([
{ id: 'custom_field_1' as any, name: 'Custom Field 1' },
])
const mockDisplayFields = [
{ id: 'custom_field_1' as any, name: 'Custom Field 1' },
]
settingsService.allDisplayFields = jest.fn(() => mockDisplayFields) as any
expect(component.getDisplayCustomFieldTitle('custom_field_1')).toEqual(
'Custom Field 1'
)
@@ -255,7 +255,7 @@ export class DocumentListComponent
}
public getDisplayCustomFieldTitle(field: string) {
return this.settingsService.allDisplayFields.find((f) => f.id === field)
return this.settingsService.allDisplayFields().find((f) => f.id === field)
?.name
}
@@ -260,7 +260,7 @@ describe('FilterEditorComponent', () => {
documentService = TestBed.inject(DocumentService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = users[0]
settingsService.currentUser.set(users[0])
permissionsService = TestBed.inject(PermissionsService)
searchService = TestBed.inject(SearchService)
jest
@@ -55,7 +55,7 @@ describe('FileDropComponent', () => {
it('should disable drag-drop if disabled in settings', fakeAsync(() => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled = false
settingsService.globalDropzoneEnabled.set(false)
expect(component.dragDropEnabled).toBeFalsy()
component.onDragOver(new Event('dragover') as DragEvent)
@@ -296,21 +296,21 @@ describe('FileDropComponent', () => {
})
it('should ignore events if disabled', fakeAsync(() => {
settingsService.globalDropzoneEnabled = false
expect(settingsService.globalDropzoneActive).toBeFalsy()
settingsService.globalDropzoneEnabled.set(false)
expect(settingsService.globalDropzoneActive()).toBeFalsy()
component.onDragOver(new Event('dragover') as DragEvent)
expect(settingsService.globalDropzoneActive).toBeFalsy()
settingsService.globalDropzoneActive = true
expect(settingsService.globalDropzoneActive()).toBeFalsy()
settingsService.globalDropzoneActive.set(true)
component.onDragLeave(new Event('dragleave') as DragEvent)
expect(settingsService.globalDropzoneActive).toBeTruthy()
expect(settingsService.globalDropzoneActive()).toBeTruthy()
component.onDrop(new Event('drop') as DragEvent)
expect(settingsService.globalDropzoneActive).toBeTruthy()
expect(settingsService.globalDropzoneActive()).toBeTruthy()
}))
it('should hide if app loses focus', fakeAsync(() => {
const leaveSpy = jest.spyOn(component, 'onDragLeave')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled = true
settingsService.globalDropzoneEnabled.set(true)
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
@@ -326,7 +326,7 @@ describe('FileDropComponent', () => {
it('should hide on window blur', fakeAsync(() => {
const leaveSpy = jest.spyOn(component, 'onDragLeave')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled = true
settingsService.globalDropzoneEnabled.set(true)
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
@@ -7,24 +7,8 @@ export abstract class LoadingComponentWithPermissions
extends ComponentWithPermissions
implements OnDestroy
{
private loadingSignal = signal(true)
private showSignal = signal(false)
public get loading(): boolean {
return this.loadingSignal()
}
public set loading(value: boolean) {
this.loadingSignal.set(value)
}
public get show(): boolean {
return this.showSignal()
}
public set show(value: boolean) {
this.showSignal.set(value)
}
readonly loading = signal(true)
readonly show = signal(false)
protected unsubscribeNotifier: Subject<any> = new Subject()
@@ -94,7 +94,7 @@ describe('CustomFieldsComponent', () => {
toastService = TestBed.inject(ToastService)
listViewService = TestBed.inject(DocumentListViewService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 0, username: 'test' }
settingsService.currentUser.set({ id: 0, username: 'test' })
fixture = TestBed.createComponent(CustomFieldsComponent)
component = fixture.componentInstance
@@ -34,7 +34,7 @@
</div>
</li>
@if (loadingAccounts) {
@if (loadingAccounts()) {
<li class="list-group-item">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
@@ -43,7 +43,7 @@
@for (account of mailAccounts; track account) {
<li class="list-group-item">
<div class="row fade" [class.show]="showAccounts">
<div class="row fade" [class.show]="showAccounts()">
<div class="col d-flex align-items-center">
<button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount) || !userCanEdit(account)">
{{account.name}}@switch (account.account_type) {
@@ -91,7 +91,7 @@
</div>
</li>
}
@if (!loadingAccounts && mailAccounts.length === 0) {
@if (!loadingAccounts() && mailAccounts.length === 0) {
<li class="list-group-item" i18n>No mail accounts defined.</li>
}
</ul>
@@ -117,19 +117,19 @@
</div>
</li>
@if (loadingRules) {
@if (loadingRules()) {
<li class="list-group-item">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</li>
}
@for (rule of mailRules; track rule) {
@for (rule of mailRules(); track rule) {
<li class="list-group-item">
<div class="row fade" [class.show]="showRules">
<div class="row fade" [class.show]="showRules()">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule) || !userCanEdit(rule)">{{rule.name}}</button></div>
<div class="col-1 d-flex align-items-center d-none d-sm-flex">{{rule.order}}</div>
<div class="col-2 d-flex align-items-center">{{ mailAccountsById.get(rule.account)?.name }}</div>
<div class="col-2 d-flex align-items-center">{{ mailAccountsById().get(rule.account)?.name }}</div>
<div class="col-2 d-flex align-items-center d-none d-sm-flex">
<div class="form-check form-switch mb-0">
<input #inputField type="checkbox" class="form-check-input cursor-pointer" [id]="rule.id+'_enable'" [(ngModel)]="rule.enabled" (change)="onMailRuleEnableToggled(rule)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }">
@@ -179,7 +179,7 @@
</div>
</li>
}
@if (!loadingRules && mailRules.length === 0) {
@if (!loadingRules() && mailRules().length === 0) {
<li class="list-group-item" i18n>No mail rules defined.</li>
}
</ul>
@@ -113,7 +113,7 @@ describe('MailComponent', () => {
permissionsService = TestBed.inject(PermissionsService)
activatedRoute = TestBed.inject(ActivatedRoute)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 1 }
settingsService.currentUser.set({ id: 1 })
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
@@ -63,25 +63,13 @@ export class MailComponent
}
private set mailAccounts(accounts: MailAccount[]) {
this.mailAccountsSignal.set(accounts)
this.mailAccountsById = new Map(
accounts.map((account) => [account.id, account])
this.mailAccountsById.set(
new Map(accounts.map((account) => [account.id, account]))
)
}
private mailAccountsByIdSignal = signal<Map<number, MailAccount>>(new Map())
public get mailAccountsById(): Map<number, MailAccount> {
return this.mailAccountsByIdSignal()
}
private set mailAccountsById(value: Map<number, MailAccount>) {
this.mailAccountsByIdSignal.set(value)
}
readonly mailAccountsById = signal<Map<number, MailAccount>>(new Map())
private mailRulesSignal = signal<MailRule[]>([])
public get mailRules(): MailRule[] {
return this.mailRulesSignal()
}
public set mailRules(value: MailRule[]) {
this.mailRulesSignal.set(value)
}
readonly mailRules = signal<MailRule[]>([])
unsubscribeNotifier: Subject<any> = new Subject()
oAuthAccountId: number
@@ -94,35 +82,10 @@ export class MailComponent
return this.settingsService.get(SETTINGS_KEYS.OUTLOOK_OAUTH_URL)
}
private loadingRulesSignal = signal(true)
private showRulesSignal = signal(false)
private loadingAccountsSignal = signal(true)
private showAccountsSignal = signal(false)
public get loadingRules(): boolean {
return this.loadingRulesSignal()
}
public set loadingRules(value: boolean) {
this.loadingRulesSignal.set(value)
}
public get showRules(): boolean {
return this.showRulesSignal()
}
public set showRules(value: boolean) {
this.showRulesSignal.set(value)
}
public get loadingAccounts(): boolean {
return this.loadingAccountsSignal()
}
public set loadingAccounts(value: boolean) {
this.loadingAccountsSignal.set(value)
}
public get showAccounts(): boolean {
return this.showAccountsSignal()
}
public set showAccounts(value: boolean) {
this.showAccountsSignal.set(value)
}
readonly loadingRules = signal(true)
readonly showRules = signal(false)
readonly loadingAccounts = signal(true)
readonly showAccounts = signal(false)
ngOnInit(): void {
this.mailAccountService
@@ -132,8 +95,8 @@ export class MailComponent
takeUntil(this.unsubscribeNotifier),
tap((r) => {
this.mailAccounts = r.results
this.loadingAccounts = false
this.showAccounts = true
this.loadingAccounts.set(false)
this.showAccounts.set(true)
if (this.oAuthAccountId) {
this.editMailAccount(
this.mailAccounts.find(
@@ -145,7 +108,7 @@ export class MailComponent
)
.subscribe({
error: (e) => {
this.loadingAccounts = false
this.loadingAccounts.set(false)
this.toastService.showError(
$localize`Error retrieving mail accounts`,
e
@@ -159,14 +122,14 @@ export class MailComponent
first(),
takeUntil(this.unsubscribeNotifier),
tap((r) => {
this.mailRules = r.results
this.loadingRules = false
this.showRules = true
this.mailRules.set(r.results)
this.loadingRules.set(false)
this.showRules.set(true)
})
)
.subscribe({
error: (e) => {
this.loadingRules = false
this.loadingRules.set(false)
this.toastService.showError($localize`Error retrieving mail rules`, e)
},
})
@@ -292,7 +255,7 @@ export class MailComponent
this.mailRuleService
.listAll(null, null, { full_perms: true })
.subscribe((r) => {
this.mailRules = r.results
this.mailRules.set(r.results)
})
})
modal.componentInstance.failed
@@ -348,7 +311,7 @@ export class MailComponent
this.mailRuleService
.listAll(null, null, { full_perms: true })
.subscribe((r) => {
this.mailRules = r.results
this.mailRules.set(r.results)
})
},
error: (e) => {
@@ -705,11 +705,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'])
@@ -17,8 +17,10 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
private settingsService = inject(SettingsService)
private documentService = inject(DocumentService)
private savedViewsSignal = signal<SavedView[]>([])
private savedViewDocumentCountsSignal = signal<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() {
@@ -26,22 +28,6 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
this.resourceName = 'saved_views'
}
private get savedViews(): SavedView[] {
return this.savedViewsSignal()
}
private set savedViews(views: SavedView[]) {
this.savedViewsSignal.set(views)
}
private get savedViewDocumentCounts(): Map<number, number> {
return this.savedViewDocumentCountsSignal()
}
private set savedViewDocumentCounts(counts: Map<number, number>) {
this.savedViewDocumentCountsSignal.set(counts)
}
public list(
page?: number,
pageSize?: number,
@@ -53,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)
},
})
)
@@ -79,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[] {
@@ -111,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
@@ -126,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)
)
@@ -192,12 +181,12 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
}
public setDocumentCount(view: SavedView, count: number) {
const counts = new Map(this.savedViewDocumentCounts)
const counts = new Map(this.savedViewDocumentCounts())
counts.set(view.id, count)
this.savedViewDocumentCounts = counts
this.savedViewDocumentCounts.set(counts)
}
public getDocumentCount(view: SavedView): number {
return this.savedViewDocumentCounts.get(view.id)
return this.savedViewDocumentCounts().get(view.id)
}
}
+13 -56
View File
@@ -296,15 +296,7 @@ export class SettingsService {
private settings: Record<string, any> = {}
private settingsVersion = signal(0)
private currentUserSignal = signal<User>(undefined)
get currentUser(): User {
return this.currentUserSignal()
}
set currentUser(value: User) {
this.currentUserSignal.set(value)
}
readonly currentUser = signal<User>(undefined)
public settingsSaved: EventEmitter<any> = new EventEmitter()
@@ -313,49 +305,14 @@ export class SettingsService {
return this._renderer
}
private dashboardIsEmptySignal = signal(false)
private globalDropzoneEnabledSignal = signal(true)
private globalDropzoneActiveSignal = signal(false)
private organizingSidebarSavedViewsSignal = signal(false)
readonly dashboardIsEmpty = signal(false)
readonly globalDropzoneEnabled = signal(true)
readonly globalDropzoneActive = signal(false)
readonly organizingSidebarSavedViews = signal(false)
public get dashboardIsEmpty(): boolean {
return this.dashboardIsEmptySignal()
}
public set dashboardIsEmpty(value: boolean) {
this.dashboardIsEmptySignal.set(value)
}
public get globalDropzoneEnabled(): boolean {
return this.globalDropzoneEnabledSignal()
}
public set globalDropzoneEnabled(value: boolean) {
this.globalDropzoneEnabledSignal.set(value)
}
public get globalDropzoneActive(): boolean {
return this.globalDropzoneActiveSignal()
}
public set globalDropzoneActive(value: boolean) {
this.globalDropzoneActiveSignal.set(value)
}
public get organizingSidebarSavedViews(): boolean {
return this.organizingSidebarSavedViewsSignal()
}
public set organizingSidebarSavedViews(value: boolean) {
this.organizingSidebarSavedViewsSignal.set(value)
}
private allDisplayFieldsSignal = signal<
Array<{ id: DisplayField; name: string }>
>(DEFAULT_DISPLAY_FIELDS)
public get allDisplayFields(): Array<{ id: DisplayField; name: string }> {
return this.allDisplayFieldsSignal()
}
readonly allDisplayFields = signal<Array<{ id: DisplayField; name: string }>>(
DEFAULT_DISPLAY_FIELDS
)
public displayFieldsInit: EventEmitter<boolean> = new EventEmitter()
constructor() {
@@ -409,7 +366,7 @@ 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
@@ -453,7 +410,7 @@ export class SettingsService {
: null
}).filter((f) => f)
this.allDisplayFieldsSignal.set(displayFields)
this.allDisplayFields.set(displayFields)
if (
this.permissionsService.currentUserCan(
@@ -481,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()
}
@@ -615,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) {
@@ -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,
@@ -211,7 +211,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 ||