Compare commits

...

3 Commits

Author SHA1 Message Date
shamoon
6e1918a425
Add setting, trigger global refresh when needed 2025-06-21 12:12:23 -07:00
shamoon
516dcdcc9b
Include count badge on dashboard widgets 2025-06-21 12:12:22 -07:00
shamoon
873f520135
Enhancement: show document count on sidebar saved views 2025-06-19 13:23:46 -07:00
15 changed files with 134 additions and 17 deletions

View File

@ -176,6 +176,7 @@
<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>

View File

@ -212,7 +212,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))

View File

@ -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,
@ -138,6 +139,7 @@ export class SettingsComponent
notificationsConsumerSuppressOnDashboard: new FormControl(null),
savedViewsWarnOnUnsavedChange: new FormControl(null),
sidebarViewsShowCount: new FormControl(null),
})
SettingsNavIDs = SettingsNavIDs
@ -192,11 +194,13 @@ export class SettingsComponent
private router: Router,
public permissionsService: PermissionsService,
private modalService: NgbModal,
private systemStatusService: SystemStatusService
private systemStatusService: SystemStatusService,
private savedViewsService: SavedViewService
) {
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

View File

@ -112,7 +112,14 @@
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>&nbsp;{{view.name}}</span>
<i-bs class="me-1" name="funnel"></i-bs><span>&nbsp;{{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>

View File

@ -121,6 +121,7 @@ describe('AppFrameComponent', () => {
results: saved_views,
}),
sidebarViews: saved_views.filter((v) => v.show_in_sidebar),
getDocumentCount: (view: SavedView) => 5,
},
},
PermissionsService,

View File

@ -35,6 +35,7 @@ import {
PermissionsService,
PermissionType,
} from 'src/app/services/permissions.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import {
AppRemoteVersion,
RemoteVersionService,
@ -91,7 +92,8 @@ export class AppFrameComponent
private readonly toastService: ToastService,
private modalService: NgbModal,
public permissionsService: PermissionsService,
private djangoMessagesService: DjangoMessagesService
private djangoMessagesService: DjangoMessagesService,
private documentService: DocumentService
) {
super()
@ -101,7 +103,9 @@ export class AppFrameComponent
PermissionType.SavedView
)
) {
this.savedViewService.reload()
this.savedViewService.reload(() => {
this.savedViewService.maybeRefreshDocumentCounts()
})
}
}
@ -282,4 +286,8 @@ export class AppFrameComponent
onLogout() {
this.openDocumentsService.closeAll()
}
get showSidebarCounts(): boolean {
return this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT)
}
}

View File

@ -1,6 +1,7 @@
<pngx-widget-frame
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
[title]="savedView.name"
[badge]="count"
[loading]="loading"
[draggable]="savedView"
>

View File

@ -121,6 +121,8 @@ export class SavedViewWidgetComponent
displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS
count: number
ngOnInit(): void {
this.reload()
this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
@ -181,6 +183,7 @@ export class SavedViewWidgetComponent
tap((result) => {
this.show = true
this.documents = result.results
this.count = result.count
}),
delay(500)
)

View File

@ -2,13 +2,16 @@
<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-light ms-2">{{badge}}</span>
}
</div>
@if (loading) {
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>

View File

@ -30,6 +30,9 @@ export class WidgetFrameComponent
@Input()
cardless: boolean = false
@Input()
badge: string
ngAfterViewInit(): void {
setTimeout(() => {
this.show = true

View File

@ -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'
@ -278,7 +279,8 @@ export class DocumentDetailComponent
private http: HttpClient,
private hotKeyService: HotKeyService,
private componentRouterService: ComponentRouterService,
private deviceDetectorService: DeviceDetectorService
private deviceDetectorService: DeviceDetectorService,
private savedViewService: SavedViewService
) {
super()
}
@ -845,6 +847,7 @@ export class DocumentDetailComponent
} else {
this.openDocumentService.refreshDocument(this.documentId)
}
this.savedViewService.maybeRefreshDocumentCounts()
},
error: (error) => {
this.networkActive = false
@ -1192,6 +1195,7 @@ export class DocumentDetailComponent
notesUpdated(notes: DocumentNote[]) {
this.document.notes = notes
this.openDocumentService.refreshDocument(this.documentId)
this.savedViewService.maybeRefreshDocumentCounts()
}
get userIsOwner(): boolean {

View File

@ -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'
@ -106,7 +107,8 @@ export class BulkEditorComponent
private toastService: ToastService,
private storagePathService: StoragePathService,
private customFieldService: CustomFieldsService,
private permissionService: PermissionsService
private permissionService: PermissionsService,
public savedViewService: SavedViewService
) {
super()
}
@ -274,6 +276,7 @@ export class BulkEditorComponent
this.list.selected.forEach((id) => {
this.openDocumentService.refreshDocument(id)
})
this.savedViewService.maybeRefreshDocumentCounts()
if (modal) {
modal.close()
}

View File

@ -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',

View File

@ -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,33 @@ 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)
})
beforeEach(() => {
// Dont need to setup again

View File

@ -1,12 +1,13 @@
import { HttpClient } from '@angular/common/http'
import { 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,10 +15,13 @@ import { AbstractPaperlessService } from './abstract-paperless-service'
export class SavedViewService extends AbstractPaperlessService<SavedView> {
public loading: boolean = true
private savedViews: SavedView[] = []
private savedViewDocumentCounts: Map<number, number> = new Map()
private unsubscribeNotifier: Subject<void> = new Subject<void>()
constructor(
protected http: HttpClient,
private settingsService: SettingsService
private settingsService: SettingsService,
private documentService: DocumentService
) {
super(http, 'saved_views')
}
@ -45,8 +49,16 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
)
}
public reload() {
this.listAll().subscribe()
public reload(callback: any = null) {
this.listAll()
.pipe(
tap((r) => {
if (callback) {
callback(r)
}
})
)
.subscribe()
}
get allViews() {
@ -109,4 +121,30 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
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)
}
}