mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-08 13:05:10 +00:00
Update some global components with loading signals
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { NgClass } from '@angular/common'
|
||||
import { Component, HostListener, inject, OnInit } from '@angular/core'
|
||||
import { Component, HostListener, inject, OnInit, signal } from '@angular/core'
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router'
|
||||
import {
|
||||
NgbCollapseModule,
|
||||
@@ -90,16 +90,48 @@ export class AppFrameComponent
|
||||
permissionsService = inject(PermissionsService)
|
||||
private djangoMessagesService = inject(DjangoMessagesService)
|
||||
|
||||
appRemoteVersion: AppRemoteVersion
|
||||
private appRemoteVersionSignal = signal<AppRemoteVersion>(null)
|
||||
|
||||
isMenuCollapsed: boolean = true
|
||||
private isMenuCollapsedSignal = signal(true)
|
||||
|
||||
slimSidebarAnimating: boolean = false
|
||||
private slimSidebarAnimatingSignal = signal(false)
|
||||
|
||||
public mobileSearchHidden: boolean = false
|
||||
private mobileSearchHiddenSignal = 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
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ViewChild,
|
||||
ViewChildren,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
@@ -82,12 +83,36 @@ export class GlobalSearchComponent implements OnInit {
|
||||
private locationStrategy = inject(LocationStrategy)
|
||||
|
||||
public DataType = DataType
|
||||
public query: string
|
||||
private querySignal = signal<string>(null)
|
||||
public queryDebounce: Subject<string>
|
||||
public searchResults: GlobalSearchResult
|
||||
private searchResultsSignal = signal<GlobalSearchResult>(null)
|
||||
private currentItemIndex: number = -1
|
||||
private domIndex: number = -1
|
||||
public loading: boolean = false
|
||||
private loadingSignal = signal(false)
|
||||
|
||||
public get query(): string {
|
||||
return this.querySignal()
|
||||
}
|
||||
|
||||
public set query(value: string) {
|
||||
this.querySignal.set(value)
|
||||
}
|
||||
|
||||
public get searchResults(): GlobalSearchResult {
|
||||
return this.searchResultsSignal()
|
||||
}
|
||||
|
||||
public set searchResults(value: GlobalSearchResult) {
|
||||
this.searchResultsSignal.set(value)
|
||||
}
|
||||
|
||||
public get loading(): boolean {
|
||||
return this.loadingSignal()
|
||||
}
|
||||
|
||||
public set loading(value: boolean) {
|
||||
this.loadingSignal.set(value)
|
||||
}
|
||||
|
||||
@ViewChild('searchInput') searchInput: ElementRef
|
||||
@ViewChild('resultsDropdown') resultsDropdown: NgbDropdown
|
||||
|
||||
+42
-7
@@ -1,11 +1,14 @@
|
||||
<pngx-widget-frame
|
||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
|
||||
[title]="savedView.name"
|
||||
[badge]="count"
|
||||
[loading]="loading"
|
||||
[loading]="false"
|
||||
[draggable]="savedView"
|
||||
>
|
||||
|
||||
@if (count !== null) {
|
||||
<span title-badge class="badge bg-info text-dark ms-2">{{count}}</span>
|
||||
}
|
||||
|
||||
@if (documents.length) {
|
||||
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
|
||||
}
|
||||
@@ -30,11 +33,24 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (loading && !documents.length) {
|
||||
@for (row of placeholderRows; track row) {
|
||||
<tr>
|
||||
@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 }">
|
||||
<div class="placeholder-glow text-start">
|
||||
<span class="placeholder bg-secondary w-50" [ngStyle]="{ opacity: 1 - (row * 1/placeholderRows.length) }"></span>
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
@for (doc of documents; track doc.id; let i = $index) {
|
||||
<tr>
|
||||
@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 && show) {
|
||||
@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>
|
||||
</div>
|
||||
@@ -115,12 +131,22 @@
|
||||
</table>
|
||||
} @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) {
|
||||
<pngx-document-card-small
|
||||
class="p-0"
|
||||
[ngStyle]="{ opacity: 1 - (row * 1/placeholderRows.length) }"
|
||||
[document]="null"
|
||||
[displayFields]="displayFields">
|
||||
</pngx-document-card-small>
|
||||
}
|
||||
}
|
||||
@for (d of documents; track d.id; let i = $index) {
|
||||
<pngx-document-card-small
|
||||
class="p-0"
|
||||
[ngStyle]="{ opacity: !loading && show ? 1 : 1 - (i * 1/documents.length) }"
|
||||
[ngStyle]="{ opacity: show ? 1 : 1 - (i * 1/documents.length) }"
|
||||
(dblClickDocument)="openDocumentDetail(d)"
|
||||
[document]="!loading && show ? d : null"
|
||||
[document]="d"
|
||||
[displayFields]="displayFields"
|
||||
(clickTag)="clickTag($event)"
|
||||
(clickCorrespondent)="clickCorrespondent($event)"
|
||||
@@ -131,11 +157,20 @@
|
||||
</div>
|
||||
} @else if (displayMode === DisplayMode.LARGE_CARDS) {
|
||||
<div class="row my-n2">
|
||||
@if (loading && !documents.length) {
|
||||
@for (row of placeholderRows; track row) {
|
||||
<pngx-document-card-large
|
||||
[document]="null"
|
||||
[ngStyle]="{ opacity: 1 - (row * 1/placeholderRows.length) }"
|
||||
[displayFields]="displayFields">
|
||||
</pngx-document-card-large>
|
||||
}
|
||||
}
|
||||
@for (d of documents; track d.id; let i = $index) {
|
||||
<pngx-document-card-large
|
||||
(dblClickDocument)="openDocumentDetail(d)"
|
||||
[document]="!loading && show ? d : null"
|
||||
[ngStyle]="{ opacity: !loading && show ? 1 : 1 - (i * 1/documents.length) }"
|
||||
[document]="d"
|
||||
[ngStyle]="{ opacity: show ? 1 : 1 - (i * 1/documents.length) }"
|
||||
[displayFields]="displayFields"
|
||||
(clickTag)="clickTag($event)"
|
||||
(clickCorrespondent)="clickCorrespondent($event)"
|
||||
|
||||
+62
-22
@@ -6,12 +6,13 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
QueryList,
|
||||
signal,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { Router, RouterModule } from '@angular/router'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { delay, Subject, takeUntil, tap } from 'rxjs'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
|
||||
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
||||
import { TagComponent } from 'src/app/components/common/tag/tag.component'
|
||||
@@ -106,7 +107,7 @@ export class SavedViewWidgetComponent
|
||||
@Input()
|
||||
savedView: SavedView
|
||||
|
||||
documents: Document[] = []
|
||||
private documentsSignal = signal<Document[]>([])
|
||||
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
@@ -116,21 +117,56 @@ export class SavedViewWidgetComponent
|
||||
mouseOnPreview = false
|
||||
popoverHidden = true
|
||||
|
||||
displayMode: DisplayMode
|
||||
private displayModeSignal = signal<DisplayMode>(null)
|
||||
|
||||
displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS
|
||||
private displayFieldsSignal = signal<DisplayField[]>(
|
||||
DEFAULT_DASHBOARD_DISPLAY_FIELDS
|
||||
)
|
||||
|
||||
count: number
|
||||
private countSignal = 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.reload()
|
||||
this.placeholderRows = Array.from(
|
||||
{
|
||||
length: this.savedView?.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
|
||||
},
|
||||
(_, index) => index
|
||||
)
|
||||
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
|
||||
this.websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.reload()
|
||||
})
|
||||
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
@@ -159,6 +195,14 @@ export class SavedViewWidgetComponent
|
||||
this.settingsService.allDisplayFields.find((f) => f.id === field) !==
|
||||
undefined
|
||||
)
|
||||
|
||||
this.reload()
|
||||
this.websocketStatusService
|
||||
.onDocumentConsumptionFinished()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.reload()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -168,6 +212,7 @@ export class SavedViewWidgetComponent
|
||||
|
||||
reload() {
|
||||
this.loading = this.documents.length == 0
|
||||
this.show = true
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
1,
|
||||
@@ -177,18 +222,13 @@ export class SavedViewWidgetComponent
|
||||
this.savedView.filter_rules,
|
||||
{ truncate_content: true }
|
||||
)
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((result) => {
|
||||
this.show = true
|
||||
this.documents = result.results
|
||||
this.count = result.count
|
||||
this.savedViewService.setDocumentCount(this.savedView, result.count)
|
||||
}),
|
||||
delay(500)
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => {
|
||||
this.documents = result.results
|
||||
this.count = result.count
|
||||
this.savedViewService.setDocumentCount(this.savedView, result.count)
|
||||
this.loading = false
|
||||
this.show = true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -1,19 +1,20 @@
|
||||
@if (!cardless) {
|
||||
<div class="card shadow-sm bg-light fade" [class.show]="show" cdkDrag [cdkDragDisabled]="!draggable" cdkDragPreviewContainer="parent">
|
||||
@if (!cardless()) {
|
||||
<div class="card shadow-sm bg-light fade" [class.show]="show" cdkDrag [cdkDragDisabled]="!draggable()" cdkDragPreviewContainer="parent">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
@if (draggable) {
|
||||
@if (draggable()) {
|
||||
<div class="ms-n2 me-1" cdkDragHandle>
|
||||
<i-bs name="grip-vertical"></i-bs>
|
||||
</div>
|
||||
}
|
||||
<h6 class="card-title mb-0">{{title}}</h6>
|
||||
@if (badge) {
|
||||
<span class="badge bg-info text-dark ms-2">{{badge}}</span>
|
||||
<h6 class="card-title mb-0">{{title()}}</h6>
|
||||
<ng-content select="[title-badge]"></ng-content>
|
||||
@if (badge() !== null && badge() !== undefined) {
|
||||
<span class="badge bg-info text-dark ms-2">{{badge()}}</span>
|
||||
}
|
||||
</div>
|
||||
@if (loading) {
|
||||
@if (loading()) {
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
}
|
||||
|
||||
+2
-4
@@ -45,21 +45,19 @@ describe('WidgetFrameComponent', () => {
|
||||
})
|
||||
|
||||
it('should show title', () => {
|
||||
component.title = 'Foo'
|
||||
fixture.componentRef.setInput('title', 'Foo')
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain('Foo')
|
||||
})
|
||||
|
||||
it('should show loading indicator', () => {
|
||||
expect(fixture.debugElement.query(By.css('.spinner-border'))).toBeNull()
|
||||
component.loading = true
|
||||
fixture.componentRef.setInput('loading', true)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('.spinner-border'))).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should show', () => {
|
||||
expect(component.show).toBeFalsy()
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(component.show).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
+15
-27
@@ -1,8 +1,7 @@
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { NgTemplateOutlet } from '@angular/common'
|
||||
import { AfterViewInit, Component, Input } from '@angular/core'
|
||||
import { AfterViewInit, Component, input, signal } from '@angular/core'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { LoadingComponentWithPermissions } from 'src/app/components/loading-component/loading.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-widget-frame',
|
||||
@@ -10,39 +9,28 @@ import { LoadingComponentWithPermissions } from 'src/app/components/loading-comp
|
||||
styleUrls: ['./widget-frame.component.scss'],
|
||||
imports: [DragDropModule, NgxBootstrapIconsModule, NgTemplateOutlet],
|
||||
})
|
||||
export class WidgetFrameComponent
|
||||
extends LoadingComponentWithPermissions
|
||||
implements AfterViewInit
|
||||
{
|
||||
constructor() {
|
||||
super()
|
||||
this.loading = false
|
||||
export class WidgetFrameComponent implements AfterViewInit {
|
||||
private showSignal = signal(false)
|
||||
|
||||
loading = input(false)
|
||||
|
||||
get show(): boolean {
|
||||
return this.showSignal()
|
||||
}
|
||||
|
||||
@Input()
|
||||
title: string
|
||||
|
||||
@Input()
|
||||
override set loading(value: boolean) {
|
||||
super.loading = value
|
||||
set show(value: boolean) {
|
||||
this.showSignal.set(value)
|
||||
}
|
||||
|
||||
override get loading(): boolean {
|
||||
return super.loading
|
||||
}
|
||||
title = input<string>()
|
||||
|
||||
@Input()
|
||||
draggable: any
|
||||
draggable = input<any>()
|
||||
|
||||
@Input()
|
||||
cardless: boolean = false
|
||||
cardless = input(false)
|
||||
|
||||
@Input()
|
||||
badge: string
|
||||
badge = input<string | number>(null)
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
setTimeout(() => {
|
||||
this.show = true
|
||||
}, 100)
|
||||
this.show = true
|
||||
}
|
||||
}
|
||||
|
||||
+1
-6
@@ -14,7 +14,6 @@ import {
|
||||
NgbTooltipModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { delay, of } from 'rxjs'
|
||||
import {
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
@@ -108,11 +107,7 @@ export class DocumentCardLargeComponent
|
||||
popoverHidden = true
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
of(true)
|
||||
.pipe(delay(50))
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
})
|
||||
this.show = true
|
||||
}
|
||||
|
||||
get searchScoreClass() {
|
||||
|
||||
+1
-7
@@ -14,8 +14,6 @@ import {
|
||||
NgbTooltipModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { of } from 'rxjs'
|
||||
import { delay } from 'rxjs/operators'
|
||||
import {
|
||||
DEFAULT_DISPLAY_FIELDS,
|
||||
DisplayField,
|
||||
@@ -101,11 +99,7 @@ export class DocumentCardSmallComponent
|
||||
@ViewChild('popupPreview') popupPreview: PreviewPopupComponent
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
of(true)
|
||||
.pipe(delay(50))
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
})
|
||||
this.show = true
|
||||
}
|
||||
|
||||
getIsThumbInverted() {
|
||||
|
||||
Reference in New Issue
Block a user