diff --git a/src-ui/src/app/components/admin/settings/settings.component.html b/src-ui/src/app/components/admin/settings/settings.component.html index 9d235a0f3..ccd3cc7e3 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.html +++ b/src-ui/src/app/components/admin/settings/settings.component.html @@ -176,6 +176,7 @@
+
diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index c6eeaf896..37908d139 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -31,6 +31,7 @@ import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' 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' @@ -72,6 +73,7 @@ describe('SettingsComponent', () => { let groupService: GroupService let modalService: NgbModal let systemStatusService: SystemStatusService + let savedViewsService: SavedViewService beforeEach(async () => { TestBed.configureTestingModule({ @@ -122,6 +124,7 @@ describe('SettingsComponent', () => { 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') @@ -212,7 +215,7 @@ describe('SettingsComponent', () => { expect(toastErrorSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled() - expect(setSpy).toHaveBeenCalledTimes(29) + expect(setSpy).toHaveBeenCalledTimes(30) // succeed storeSpy.mockReturnValueOnce(of(true)) @@ -345,4 +348,14 @@ describe('SettingsComponent', () => { 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() + }) }) diff --git a/src-ui/src/app/components/admin/settings/settings.component.ts b/src-ui/src/app/components/admin/settings/settings.component.ts index 7cfe926ad..26c0e1b88 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.ts @@ -49,6 +49,7 @@ 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 { LanguageOption, @@ -117,6 +118,7 @@ export class SettingsComponent permissionsService = inject(PermissionsService) private modalService = inject(NgbModal) private systemStatusService = inject(SystemStatusService) + private savedViewsService = inject(SavedViewService) activeNavID: number @@ -152,6 +154,7 @@ export class SettingsComponent notificationsConsumerSuppressOnDashboard: new FormControl(null), savedViewsWarnOnUnsavedChange: new FormControl(null), + sidebarViewsShowCount: new FormControl(null), }) SettingsNavIDs = SettingsNavIDs @@ -197,6 +200,7 @@ export class SettingsComponent super() this.settings.settingsSaved.subscribe(() => { if (!this.savePending) this.initialize() + this.savedViewsService.maybeRefreshDocumentCounts() }) } @@ -308,6 +312,9 @@ export class SettingsComponent 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 @@ -485,6 +492,10 @@ export class SettingsComponent 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 diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index ff80288aa..abf47d459 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -112,7 +112,14 @@ routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> -  {{view.name}} +  {{view.name}} + @if (showSidebarCounts && !slimSidebarEnabled) { + {{ savedViewService.getDocumentCount(view) }} + } + + @if (showSidebarCounts && slimSidebarEnabled) { + {{ savedViewService.getDocumentCount(view) }} + } @if (settingsService.organizingSidebarSavedViews) {
diff --git a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts index f1d54ba70..0c1de7891 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.spec.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.spec.ts @@ -92,6 +92,7 @@ describe('AppFrameComponent', () => { let router: Router let savedViewSpy let modalService: NgbModal + let maybeRefreshSpy beforeEach(async () => { TestBed.configureTestingModule({ @@ -113,7 +114,11 @@ describe('AppFrameComponent', () => { { provide: SavedViewService, useValue: { - reload: () => {}, + reload: (fn: any) => { + if (fn) { + fn() + } + }, listAll: () => of({ all: [saved_views.map((v) => v.id)], @@ -121,6 +126,8 @@ describe('AppFrameComponent', () => { results: saved_views, }), sidebarViews: saved_views.filter((v) => v.show_in_sidebar), + getDocumentCount: (view: SavedView) => 5, + maybeRefreshDocumentCounts: () => {}, }, }, PermissionsService, @@ -169,6 +176,7 @@ describe('AppFrameComponent', () => { jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) savedViewSpy = jest.spyOn(savedViewService, 'reload') + maybeRefreshSpy = jest.spyOn(savedViewService, 'maybeRefreshDocumentCounts') fixture = TestBed.createComponent(AppFrameComponent) component = fixture.componentInstance @@ -359,4 +367,8 @@ describe('AppFrameComponent', () => { expect(toastErrorSpy).toHaveBeenCalledTimes(2) expect(toastInfoSpy).toHaveBeenCalledTimes(3) }) + + it('should call maybeRefreshDocumentCounts after saved views reload', () => { + expect(maybeRefreshSpy).toHaveBeenCalled() + }) }) diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts index df3732969..35b5b5bdc 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.ts +++ b/src-ui/src/app/components/app-frame/app-frame.component.ts @@ -102,7 +102,9 @@ export class AppFrameComponent PermissionType.SavedView ) ) { - this.savedViewService.reload() + this.savedViewService.reload(() => { + this.savedViewService.maybeRefreshDocumentCounts() + }) } } @@ -283,4 +285,8 @@ export class AppFrameComponent onLogout() { this.openDocumentsService.closeAll() } + + get showSidebarCounts(): boolean { + return this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT) + } } diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html index 53fa86dd3..ef82a96a3 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -1,6 +1,7 @@ diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts index 3a808bf9a..f24e988f4 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts @@ -118,6 +118,8 @@ export class SavedViewWidgetComponent displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS + count: number + ngOnInit(): void { this.reload() this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE @@ -178,6 +180,7 @@ export class SavedViewWidgetComponent tap((result) => { this.show = true this.documents = result.results + this.count = result.count }), delay(500) ) diff --git a/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html b/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html index 101a489b9..45dcdf7d8 100644 --- a/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html +++ b/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html @@ -2,13 +2,16 @@
-
+
@if (draggable) {
}
{{title}}
+ @if (badge) { + {{badge}} + }
@if (loading) {
diff --git a/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.ts b/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.ts index 728787e9e..a638cb52c 100644 --- a/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.ts @@ -30,6 +30,9 @@ export class WidgetFrameComponent @Input() cardless: boolean = false + @Input() + badge: string + ngAfterViewInit(): void { setTimeout(() => { this.show = true diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index e8a05962c..3f51712ed 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -73,6 +73,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic 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' @@ -195,6 +196,7 @@ export class DocumentDetailComponent private hotKeyService = inject(HotKeyService) private componentRouterService = inject(ComponentRouterService) private deviceDetectorService = inject(DeviceDetectorService) + private savedViewService = inject(SavedViewService) @ViewChild('inputTitle') titleInput: TextComponent @@ -841,6 +843,7 @@ export class DocumentDetailComponent } else { this.openDocumentService.refreshDocument(this.documentId) } + this.savedViewService.maybeRefreshDocumentCounts() }, error: (error) => { this.networkActive = false @@ -1188,6 +1191,7 @@ export class DocumentDetailComponent notesUpdated(notes: DocumentNote[]) { this.document.notes = notes this.openDocumentService.refreshDocument(this.documentId) + this.savedViewService.maybeRefreshDocumentCounts() } get userIsOwner(): boolean { diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 5d31eb1aa..4e7380144 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -32,6 +32,7 @@ import { 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' @@ -83,6 +84,7 @@ export class BulkEditorComponent private storagePathService = inject(StoragePathService) private customFieldService = inject(CustomFieldsService) private permissionService = inject(PermissionsService) + private savedViewService = inject(SavedViewService) tagSelectionModel = new FilterableDropdownSelectionModel(true) correspondentSelectionModel = new FilterableDropdownSelectionModel() @@ -270,6 +272,7 @@ export class BulkEditorComponent this.list.selected.forEach((id) => { this.openDocumentService.refreshDocument(id) }) + this.savedViewService.maybeRefreshDocumentCounts() if (modal) { modal.close() } diff --git a/src-ui/src/app/data/ui-settings.ts b/src-ui/src/app/data/ui-settings.ts index e3cdeabae..6ace74810 100644 --- a/src-ui/src/app/data/ui-settings.ts +++ b/src-ui/src/app/data/ui-settings.ts @@ -58,6 +58,8 @@ export const SETTINGS_KEYS = { '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', @@ -227,6 +229,11 @@ export const SETTINGS: UiSetting[] = [ type: 'array', default: [], }, + { + key: SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT, + type: 'boolean', + default: true, + }, { key: SETTINGS_KEYS.APP_LOGO, type: 'string', diff --git a/src-ui/src/app/services/rest/saved-view.service.spec.ts b/src-ui/src/app/services/rest/saved-view.service.spec.ts index cc206de08..585425ecc 100644 --- a/src-ui/src/app/services/rest/saved-view.service.spec.ts +++ b/src-ui/src/app/services/rest/saved-view.service.spec.ts @@ -17,7 +17,7 @@ const saved_views = [ id: 1, show_on_dashboard: true, show_in_sidebar: true, - sort_field: 'name', + sort_field: 'title', sort_reverse: true, filter_rules: [], }, @@ -26,7 +26,7 @@ const saved_views = [ id: 2, show_on_dashboard: true, show_in_sidebar: true, - sort_field: 'name', + sort_field: 'created', sort_reverse: true, filter_rules: [], }, @@ -35,7 +35,7 @@ const saved_views = [ id: 3, show_on_dashboard: true, show_in_sidebar: true, - sort_field: 'name', + sort_field: 'added', sort_reverse: true, filter_rules: [], }, @@ -44,7 +44,7 @@ const saved_views = [ id: 4, show_on_dashboard: false, show_in_sidebar: false, - sort_field: 'name', + sort_field: 'owner', sort_reverse: true, filter_rules: [], }, @@ -222,6 +222,43 @@ describe(`Additional service tests for SavedViewService`, () => { }) }) + 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 diff --git a/src-ui/src/app/services/rest/saved-view.service.ts b/src-ui/src/app/services/rest/saved-view.service.ts index 11ebb6398..a8f420255 100644 --- a/src-ui/src/app/services/rest/saved-view.service.ts +++ b/src-ui/src/app/services/rest/saved-view.service.ts @@ -1,12 +1,13 @@ 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', @@ -14,9 +15,12 @@ import { AbstractPaperlessService } from './abstract-paperless-service' export class SavedViewService extends AbstractPaperlessService { protected http: HttpClient private settingsService = inject(SettingsService) + private documentService = inject(DocumentService) public loading: boolean = true private savedViews: SavedView[] = [] + private savedViewDocumentCounts: Map = new Map() + private unsubscribeNotifier: Subject = new Subject() constructor() { super() @@ -46,8 +50,16 @@ export class SavedViewService extends AbstractPaperlessService { ) } - public reload() { - this.listAll().subscribe() + public reload(callback: any = null) { + this.listAll() + .pipe( + tap((r) => { + if (callback) { + callback(r) + } + }) + ) + .subscribe() } get allViews() { @@ -110,4 +122,30 @@ export class SavedViewService extends AbstractPaperlessService { 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) => { + this.savedViewDocumentCounts.set(view.id, results.count) + }) + }) + } + + public getDocumentCount(view: SavedView): number { + return this.savedViewDocumentCounts.get(view.id) + } }