<div class="row">
<div class="col">
<pngx-input-check i18n-title title="Show warning when closing saved views with unsaved changes" formControlName="savedViewsWarnOnUnsavedChange"></pngx-input-check>
+ <pngx-input-check i18n-title title="Show document counts in sidebar saved views" formControlName="sidebarViewsShowCount"></pngx-input-check>
</div>
</div>
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
+import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
let groupService: GroupService
let modalService: NgbModal
let systemStatusService: SystemStatusService
+ let savedViewsService: SavedViewService
beforeEach(async () => {
TestBed.configureTestingModule({
permissionsService = TestBed.inject(PermissionsService)
modalService = TestBed.inject(NgbModal)
systemStatusService = TestBed.inject(SystemStatusService)
+ savedViewsService = TestBed.inject(SavedViewService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
- expect(setSpy).toHaveBeenCalledTimes(29)
+ expect(setSpy).toHaveBeenCalledTimes(30)
// succeed
storeSpy.mockReturnValueOnce(of(true))
component.reset()
expect(component.settingsForm.get('themeColor').value).toEqual('')
})
+
+ it('should trigger maybeRefreshDocumentCounts on settings save', () => {
+ completeSetup()
+ const maybeRefreshSpy = jest.spyOn(
+ savedViewsService,
+ 'maybeRefreshDocumentCounts'
+ )
+ settingsService.settingsSaved.emit(true)
+ expect(maybeRefreshSpy).toHaveBeenCalled()
+ })
})
PermissionsService,
} from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
+import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { UserService } from 'src/app/services/rest/user.service'
import {
LanguageOption,
permissionsService = inject(PermissionsService)
private modalService = inject(NgbModal)
private systemStatusService = inject(SystemStatusService)
+ private savedViewsService = inject(SavedViewService)
activeNavID: number
notificationsConsumerSuppressOnDashboard: new FormControl(null),
savedViewsWarnOnUnsavedChange: new FormControl(null),
+ sidebarViewsShowCount: new FormControl(null),
})
SettingsNavIDs = SettingsNavIDs
super()
this.settings.settingsSaved.subscribe(() => {
if (!this.savePending) this.initialize()
+ this.savedViewsService.maybeRefreshDocumentCounts()
})
}
savedViewsWarnOnUnsavedChange: this.settings.get(
SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE
),
+ sidebarViewsShowCount: this.settings.get(
+ SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT
+ ),
defaultPermsOwner: this.settings.get(SETTINGS_KEYS.DEFAULT_PERMS_OWNER),
defaultPermsViewUsers: this.settings.get(
SETTINGS_KEYS.DEFAULT_PERMS_VIEW_USERS
SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE,
this.settingsForm.value.savedViewsWarnOnUnsavedChange
)
+ this.settings.set(
+ SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT,
+ this.settingsForm.value.sidebarViewsShowCount
+ )
this.settings.set(
SETTINGS_KEYS.DEFAULT_PERMS_OWNER,
this.settingsForm.value.defaultPermsOwner
routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
[disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
popoverClass="popover-slim">
- <i-bs class="me-1" name="funnel"></i-bs><span> {{view.name}}</span>
+ <i-bs class="me-1" name="funnel"></i-bs><span> {{view.name}}
+ @if (showSidebarCounts && !slimSidebarEnabled) {
+ <span><span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span></span>
+ }
+ </span>
+ @if (showSidebarCounts && slimSidebarEnabled) {
+ <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) {
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
let router: Router
let savedViewSpy
let modalService: NgbModal
+ let maybeRefreshSpy
beforeEach(async () => {
TestBed.configureTestingModule({
{
provide: SavedViewService,
useValue: {
- reload: () => {},
+ reload: (fn: any) => {
+ if (fn) {
+ fn()
+ }
+ },
listAll: () =>
of({
all: [saved_views.map((v) => v.id)],
results: saved_views,
}),
sidebarViews: saved_views.filter((v) => v.show_in_sidebar),
+ getDocumentCount: (view: SavedView) => 5,
+ maybeRefreshDocumentCounts: () => {},
},
},
PermissionsService,
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
savedViewSpy = jest.spyOn(savedViewService, 'reload')
+ maybeRefreshSpy = jest.spyOn(savedViewService, 'maybeRefreshDocumentCounts')
fixture = TestBed.createComponent(AppFrameComponent)
component = fixture.componentInstance
expect(toastErrorSpy).toHaveBeenCalledTimes(2)
expect(toastInfoSpy).toHaveBeenCalledTimes(3)
})
+
+ it('should call maybeRefreshDocumentCounts after saved views reload', () => {
+ expect(maybeRefreshSpy).toHaveBeenCalled()
+ })
})
PermissionType.SavedView
)
) {
- this.savedViewService.reload()
+ this.savedViewService.reload(() => {
+ this.savedViewService.maybeRefreshDocumentCounts()
+ })
}
}
onLogout() {
this.openDocumentsService.closeAll()
}
+
+ get showSidebarCounts(): boolean {
+ return this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT)
+ }
}
<pngx-widget-frame
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
[title]="savedView.name"
+ [badge]="count"
[loading]="loading"
[draggable]="savedView"
>
displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS
+ count: number
+
ngOnInit(): void {
this.reload()
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
tap((result) => {
this.show = true
this.documents = result.results
+ this.count = result.count
}),
delay(500)
)
<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">
+ <div class="d-flex align-items-center">
@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>
+ }
</div>
@if (loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
@Input()
cardless: boolean = false
+ @Input()
+ badge: string
+
ngAfterViewInit(): void {
setTimeout(() => {
this.show = true
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { DocumentService } from 'src/app/services/rest/document.service'
+import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
private hotKeyService = inject(HotKeyService)
private componentRouterService = inject(ComponentRouterService)
private deviceDetectorService = inject(DeviceDetectorService)
+ private savedViewService = inject(SavedViewService)
@ViewChild('inputTitle')
titleInput: TextComponent
} else {
this.openDocumentService.refreshDocument(this.documentId)
}
+ this.savedViewService.maybeRefreshDocumentCounts()
},
error: (error) => {
this.networkActive = false
notesUpdated(notes: DocumentNote[]) {
this.document.notes = notes
this.openDocumentService.refreshDocument(this.documentId)
+ this.savedViewService.maybeRefreshDocumentCounts()
}
get userIsOwner(): boolean {
DocumentService,
SelectionDataItem,
} from 'src/app/services/rest/document.service'
+import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { SettingsService } from 'src/app/services/settings.service'
private storagePathService = inject(StoragePathService)
private customFieldService = inject(CustomFieldsService)
private permissionService = inject(PermissionsService)
+ private savedViewService = inject(SavedViewService)
tagSelectionModel = new FilterableDropdownSelectionModel(true)
correspondentSelectionModel = new FilterableDropdownSelectionModel()
this.list.selected.forEach((id) => {
this.openDocumentService.refreshDocument(id)
})
+ this.savedViewService.maybeRefreshDocumentCounts()
if (modal) {
modal.close()
}
'general-settings:saved-views:dashboard-views-sort-order',
SIDEBAR_VIEWS_SORT_ORDER:
'general-settings:saved-views:sidebar-views-sort-order',
+ SIDEBAR_VIEWS_SHOW_COUNT:
+ 'general-settings:saved-views:sidebar-views-show-count',
TOUR_COMPLETE: 'general-settings:tour-complete',
DEFAULT_PERMS_OWNER: 'general-settings:permissions:default-owner',
DEFAULT_PERMS_VIEW_USERS: 'general-settings:permissions:default-view-users',
type: 'array',
default: [],
},
+ {
+ key: SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT,
+ type: 'boolean',
+ default: true,
+ },
{
key: SETTINGS_KEYS.APP_LOGO,
type: 'string',
id: 1,
show_on_dashboard: true,
show_in_sidebar: true,
- sort_field: 'name',
+ sort_field: 'title',
sort_reverse: true,
filter_rules: [],
},
id: 2,
show_on_dashboard: true,
show_in_sidebar: true,
- sort_field: 'name',
+ sort_field: 'created',
sort_reverse: true,
filter_rules: [],
},
id: 3,
show_on_dashboard: true,
show_in_sidebar: true,
- sort_field: 'name',
+ sort_field: 'added',
sort_reverse: true,
filter_rules: [],
},
id: 4,
show_on_dashboard: false,
show_in_sidebar: false,
- sort_field: 'name',
+ sort_field: 'owner',
sort_reverse: true,
filter_rules: [],
},
})
})
+ it('should accept a callback for reload', () => {
+ const reloadSpy = jest.fn()
+ service.reload(reloadSpy)
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000`
+ )
+ req.flush({
+ results: saved_views,
+ })
+ expect(reloadSpy).toHaveBeenCalled()
+ })
+
+ it('should support getting document counts for views', () => {
+ service.maybeRefreshDocumentCounts(saved_views)
+ saved_views.forEach((saved_view) => {
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=1&ordering=-${saved_view.sort_field}&fields=id&truncate_content=true`
+ )
+ req.flush({
+ all: [],
+ count: 1,
+ results: [{ id: 1 }],
+ })
+ })
+ expect(service.getDocumentCount(saved_views[0])).toEqual(1)
+ })
+
+ it('should not refresh document counts if setting is disabled', () => {
+ jest.spyOn(settingsService, 'get').mockImplementation((key) => {
+ if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT) return false
+ })
+ service.maybeRefreshDocumentCounts(saved_views)
+ httpTestingController.expectNone(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=1&ordering=-${saved_views[0].sort_field}&fields=id&truncate_content=true`
+ )
+ })
+
beforeEach(() => {
// Dont need to setup again
import { HttpClient } from '@angular/common/http'
import { inject, Injectable } from '@angular/core'
-import { combineLatest, Observable } from 'rxjs'
-import { tap } from 'rxjs/operators'
+import { combineLatest, Observable, Subject } from 'rxjs'
+import { takeUntil, tap } from 'rxjs/operators'
import { Results } from 'src/app/data/results'
import { SavedView } from 'src/app/data/saved-view'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { SettingsService } from '../settings.service'
import { AbstractPaperlessService } from './abstract-paperless-service'
+import { DocumentService } from './document.service'
@Injectable({
providedIn: 'root',
export class SavedViewService extends AbstractPaperlessService<SavedView> {
protected http: HttpClient
private settingsService = inject(SettingsService)
+ private documentService = inject(DocumentService)
public loading: boolean = true
private savedViews: SavedView[] = []
+ private savedViewDocumentCounts: Map<number, number> = new Map()
+ private unsubscribeNotifier: Subject<void> = new Subject<void>()
constructor() {
super()
)
}
- public reload() {
- this.listAll().subscribe()
+ public reload(callback: any = null) {
+ this.listAll()
+ .pipe(
+ tap((r) => {
+ if (callback) {
+ callback(r)
+ }
+ })
+ )
+ .subscribe()
}
get allViews() {
delete(o: SavedView) {
return super.delete(o).pipe(tap(() => this.reload()))
}
+
+ public maybeRefreshDocumentCounts(views: SavedView[] = this.sidebarViews) {
+ if (!this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT)) {
+ return
+ }
+ this.unsubscribeNotifier.next() // clear previous subscriptions
+ views.forEach((view) => {
+ this.documentService
+ .listFiltered(
+ 1,
+ 1,
+ view.sort_field,
+ view.sort_reverse,
+ view.filter_rules,
+ { fields: 'id', truncate_content: true }
+ )
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe((results: Results<Document>) => {
+ this.savedViewDocumentCounts.set(view.id, results.count)
+ })
+ })
+ }
+
+ public getDocumentCount(view: SavedView): number {
+ return this.savedViewDocumentCounts.get(view.id)
+ }
}