mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-04 03:27:12 -05:00 
			
		
		
		
	Enhancement: display saved view counts (#10246)
This commit is contained in:
		
							parent
							
								
									1fe8599266
								
							
						
					
					
						commit
						293c84d871
					
				@ -176,6 +176,7 @@
 | 
				
			|||||||
            <div class="row">
 | 
					            <div class="row">
 | 
				
			||||||
              <div class="col">
 | 
					              <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 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>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -31,6 +31,7 @@ import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
 | 
				
			|||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
 | 
					import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
 | 
				
			||||||
import { PermissionsService } from 'src/app/services/permissions.service'
 | 
					import { PermissionsService } from 'src/app/services/permissions.service'
 | 
				
			||||||
import { GroupService } from 'src/app/services/rest/group.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 { UserService } from 'src/app/services/rest/user.service'
 | 
				
			||||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
					import { SettingsService } from 'src/app/services/settings.service'
 | 
				
			||||||
import { SystemStatusService } from 'src/app/services/system-status.service'
 | 
					import { SystemStatusService } from 'src/app/services/system-status.service'
 | 
				
			||||||
@ -72,6 +73,7 @@ describe('SettingsComponent', () => {
 | 
				
			|||||||
  let groupService: GroupService
 | 
					  let groupService: GroupService
 | 
				
			||||||
  let modalService: NgbModal
 | 
					  let modalService: NgbModal
 | 
				
			||||||
  let systemStatusService: SystemStatusService
 | 
					  let systemStatusService: SystemStatusService
 | 
				
			||||||
 | 
					  let savedViewsService: SavedViewService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    TestBed.configureTestingModule({
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
@ -122,6 +124,7 @@ describe('SettingsComponent', () => {
 | 
				
			|||||||
    permissionsService = TestBed.inject(PermissionsService)
 | 
					    permissionsService = TestBed.inject(PermissionsService)
 | 
				
			||||||
    modalService = TestBed.inject(NgbModal)
 | 
					    modalService = TestBed.inject(NgbModal)
 | 
				
			||||||
    systemStatusService = TestBed.inject(SystemStatusService)
 | 
					    systemStatusService = TestBed.inject(SystemStatusService)
 | 
				
			||||||
 | 
					    savedViewsService = TestBed.inject(SavedViewService)
 | 
				
			||||||
    jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
 | 
					    jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
 | 
				
			||||||
    jest
 | 
					    jest
 | 
				
			||||||
      .spyOn(permissionsService, 'currentUserHasObjectPermissions')
 | 
					      .spyOn(permissionsService, 'currentUserHasObjectPermissions')
 | 
				
			||||||
@ -212,7 +215,7 @@ describe('SettingsComponent', () => {
 | 
				
			|||||||
    expect(toastErrorSpy).toHaveBeenCalled()
 | 
					    expect(toastErrorSpy).toHaveBeenCalled()
 | 
				
			||||||
    expect(storeSpy).toHaveBeenCalled()
 | 
					    expect(storeSpy).toHaveBeenCalled()
 | 
				
			||||||
    expect(appearanceSettingsSpy).not.toHaveBeenCalled()
 | 
					    expect(appearanceSettingsSpy).not.toHaveBeenCalled()
 | 
				
			||||||
    expect(setSpy).toHaveBeenCalledTimes(29)
 | 
					    expect(setSpy).toHaveBeenCalledTimes(30)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // succeed
 | 
					    // succeed
 | 
				
			||||||
    storeSpy.mockReturnValueOnce(of(true))
 | 
					    storeSpy.mockReturnValueOnce(of(true))
 | 
				
			||||||
@ -345,4 +348,14 @@ describe('SettingsComponent', () => {
 | 
				
			|||||||
    component.reset()
 | 
					    component.reset()
 | 
				
			||||||
    expect(component.settingsForm.get('themeColor').value).toEqual('')
 | 
					    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()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
				
			|||||||
@ -49,6 +49,7 @@ import {
 | 
				
			|||||||
  PermissionsService,
 | 
					  PermissionsService,
 | 
				
			||||||
} from 'src/app/services/permissions.service'
 | 
					} from 'src/app/services/permissions.service'
 | 
				
			||||||
import { GroupService } from 'src/app/services/rest/group.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 { UserService } from 'src/app/services/rest/user.service'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  LanguageOption,
 | 
					  LanguageOption,
 | 
				
			||||||
@ -117,6 +118,7 @@ export class SettingsComponent
 | 
				
			|||||||
  permissionsService = inject(PermissionsService)
 | 
					  permissionsService = inject(PermissionsService)
 | 
				
			||||||
  private modalService = inject(NgbModal)
 | 
					  private modalService = inject(NgbModal)
 | 
				
			||||||
  private systemStatusService = inject(SystemStatusService)
 | 
					  private systemStatusService = inject(SystemStatusService)
 | 
				
			||||||
 | 
					  private savedViewsService = inject(SavedViewService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  activeNavID: number
 | 
					  activeNavID: number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -152,6 +154,7 @@ export class SettingsComponent
 | 
				
			|||||||
    notificationsConsumerSuppressOnDashboard: new FormControl(null),
 | 
					    notificationsConsumerSuppressOnDashboard: new FormControl(null),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    savedViewsWarnOnUnsavedChange: new FormControl(null),
 | 
					    savedViewsWarnOnUnsavedChange: new FormControl(null),
 | 
				
			||||||
 | 
					    sidebarViewsShowCount: new FormControl(null),
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SettingsNavIDs = SettingsNavIDs
 | 
					  SettingsNavIDs = SettingsNavIDs
 | 
				
			||||||
@ -197,6 +200,7 @@ export class SettingsComponent
 | 
				
			|||||||
    super()
 | 
					    super()
 | 
				
			||||||
    this.settings.settingsSaved.subscribe(() => {
 | 
					    this.settings.settingsSaved.subscribe(() => {
 | 
				
			||||||
      if (!this.savePending) this.initialize()
 | 
					      if (!this.savePending) this.initialize()
 | 
				
			||||||
 | 
					      this.savedViewsService.maybeRefreshDocumentCounts()
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -308,6 +312,9 @@ export class SettingsComponent
 | 
				
			|||||||
      savedViewsWarnOnUnsavedChange: this.settings.get(
 | 
					      savedViewsWarnOnUnsavedChange: this.settings.get(
 | 
				
			||||||
        SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE
 | 
					        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),
 | 
					      defaultPermsOwner: this.settings.get(SETTINGS_KEYS.DEFAULT_PERMS_OWNER),
 | 
				
			||||||
      defaultPermsViewUsers: this.settings.get(
 | 
					      defaultPermsViewUsers: this.settings.get(
 | 
				
			||||||
        SETTINGS_KEYS.DEFAULT_PERMS_VIEW_USERS
 | 
					        SETTINGS_KEYS.DEFAULT_PERMS_VIEW_USERS
 | 
				
			||||||
@ -485,6 +492,10 @@ export class SettingsComponent
 | 
				
			|||||||
      SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE,
 | 
					      SETTINGS_KEYS.SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE,
 | 
				
			||||||
      this.settingsForm.value.savedViewsWarnOnUnsavedChange
 | 
					      this.settingsForm.value.savedViewsWarnOnUnsavedChange
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    this.settings.set(
 | 
				
			||||||
 | 
					      SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT,
 | 
				
			||||||
 | 
					      this.settingsForm.value.sidebarViewsShowCount
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    this.settings.set(
 | 
					    this.settings.set(
 | 
				
			||||||
      SETTINGS_KEYS.DEFAULT_PERMS_OWNER,
 | 
					      SETTINGS_KEYS.DEFAULT_PERMS_OWNER,
 | 
				
			||||||
      this.settingsForm.value.defaultPermsOwner
 | 
					      this.settingsForm.value.defaultPermsOwner
 | 
				
			||||||
 | 
				
			|||||||
@ -112,7 +112,14 @@
 | 
				
			|||||||
                    routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
 | 
					                    routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name"
 | 
				
			||||||
                    [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
 | 
					                    [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave"
 | 
				
			||||||
                    popoverClass="popover-slim">
 | 
					                    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>
 | 
					                  </a>
 | 
				
			||||||
                  @if (settingsService.organizingSidebarSavedViews) {
 | 
					                  @if (settingsService.organizingSidebarSavedViews) {
 | 
				
			||||||
                    <div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
 | 
					                    <div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
 | 
				
			||||||
 | 
				
			|||||||
@ -92,6 +92,7 @@ describe('AppFrameComponent', () => {
 | 
				
			|||||||
  let router: Router
 | 
					  let router: Router
 | 
				
			||||||
  let savedViewSpy
 | 
					  let savedViewSpy
 | 
				
			||||||
  let modalService: NgbModal
 | 
					  let modalService: NgbModal
 | 
				
			||||||
 | 
					  let maybeRefreshSpy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(async () => {
 | 
					  beforeEach(async () => {
 | 
				
			||||||
    TestBed.configureTestingModule({
 | 
					    TestBed.configureTestingModule({
 | 
				
			||||||
@ -113,7 +114,11 @@ describe('AppFrameComponent', () => {
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
          provide: SavedViewService,
 | 
					          provide: SavedViewService,
 | 
				
			||||||
          useValue: {
 | 
					          useValue: {
 | 
				
			||||||
            reload: () => {},
 | 
					            reload: (fn: any) => {
 | 
				
			||||||
 | 
					              if (fn) {
 | 
				
			||||||
 | 
					                fn()
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
            listAll: () =>
 | 
					            listAll: () =>
 | 
				
			||||||
              of({
 | 
					              of({
 | 
				
			||||||
                all: [saved_views.map((v) => v.id)],
 | 
					                all: [saved_views.map((v) => v.id)],
 | 
				
			||||||
@ -121,6 +126,8 @@ describe('AppFrameComponent', () => {
 | 
				
			|||||||
                results: saved_views,
 | 
					                results: saved_views,
 | 
				
			||||||
              }),
 | 
					              }),
 | 
				
			||||||
            sidebarViews: saved_views.filter((v) => v.show_in_sidebar),
 | 
					            sidebarViews: saved_views.filter((v) => v.show_in_sidebar),
 | 
				
			||||||
 | 
					            getDocumentCount: (view: SavedView) => 5,
 | 
				
			||||||
 | 
					            maybeRefreshDocumentCounts: () => {},
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        PermissionsService,
 | 
					        PermissionsService,
 | 
				
			||||||
@ -169,6 +176,7 @@ describe('AppFrameComponent', () => {
 | 
				
			|||||||
    jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
 | 
					    jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    savedViewSpy = jest.spyOn(savedViewService, 'reload')
 | 
					    savedViewSpy = jest.spyOn(savedViewService, 'reload')
 | 
				
			||||||
 | 
					    maybeRefreshSpy = jest.spyOn(savedViewService, 'maybeRefreshDocumentCounts')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fixture = TestBed.createComponent(AppFrameComponent)
 | 
					    fixture = TestBed.createComponent(AppFrameComponent)
 | 
				
			||||||
    component = fixture.componentInstance
 | 
					    component = fixture.componentInstance
 | 
				
			||||||
@ -359,4 +367,8 @@ describe('AppFrameComponent', () => {
 | 
				
			|||||||
    expect(toastErrorSpy).toHaveBeenCalledTimes(2)
 | 
					    expect(toastErrorSpy).toHaveBeenCalledTimes(2)
 | 
				
			||||||
    expect(toastInfoSpy).toHaveBeenCalledTimes(3)
 | 
					    expect(toastInfoSpy).toHaveBeenCalledTimes(3)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should call maybeRefreshDocumentCounts after saved views reload', () => {
 | 
				
			||||||
 | 
					    expect(maybeRefreshSpy).toHaveBeenCalled()
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
				
			|||||||
@ -102,7 +102,9 @@ export class AppFrameComponent
 | 
				
			|||||||
        PermissionType.SavedView
 | 
					        PermissionType.SavedView
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      this.savedViewService.reload()
 | 
					      this.savedViewService.reload(() => {
 | 
				
			||||||
 | 
					        this.savedViewService.maybeRefreshDocumentCounts()
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -283,4 +285,8 @@ export class AppFrameComponent
 | 
				
			|||||||
  onLogout() {
 | 
					  onLogout() {
 | 
				
			||||||
    this.openDocumentsService.closeAll()
 | 
					    this.openDocumentsService.closeAll()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get showSidebarCounts(): boolean {
 | 
				
			||||||
 | 
					    return this.settingsService.get(SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
<pngx-widget-frame
 | 
					<pngx-widget-frame
 | 
				
			||||||
  *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
 | 
					  *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
 | 
				
			||||||
  [title]="savedView.name"
 | 
					  [title]="savedView.name"
 | 
				
			||||||
 | 
					  [badge]="count"
 | 
				
			||||||
  [loading]="loading"
 | 
					  [loading]="loading"
 | 
				
			||||||
  [draggable]="savedView"
 | 
					  [draggable]="savedView"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
 | 
				
			|||||||
@ -118,6 +118,8 @@ export class SavedViewWidgetComponent
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS
 | 
					  displayFields: DisplayField[] = DEFAULT_DASHBOARD_DISPLAY_FIELDS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  count: number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit(): void {
 | 
					  ngOnInit(): void {
 | 
				
			||||||
    this.reload()
 | 
					    this.reload()
 | 
				
			||||||
    this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
 | 
					    this.displayMode = this.savedView.display_mode ?? DisplayMode.TABLE
 | 
				
			||||||
@ -178,6 +180,7 @@ export class SavedViewWidgetComponent
 | 
				
			|||||||
        tap((result) => {
 | 
					        tap((result) => {
 | 
				
			||||||
          this.show = true
 | 
					          this.show = true
 | 
				
			||||||
          this.documents = result.results
 | 
					          this.documents = result.results
 | 
				
			||||||
 | 
					          this.count = result.count
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        delay(500)
 | 
					        delay(500)
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
 | 
				
			|||||||
@ -2,13 +2,16 @@
 | 
				
			|||||||
  <div class="card shadow-sm bg-light fade" [class.show]="show" cdkDrag [cdkDragDisabled]="!draggable" cdkDragPreviewContainer="parent">
 | 
					  <div class="card shadow-sm bg-light fade" [class.show]="show" cdkDrag [cdkDragDisabled]="!draggable" cdkDragPreviewContainer="parent">
 | 
				
			||||||
    <div class="card-header">
 | 
					    <div class="card-header">
 | 
				
			||||||
      <div class="d-flex justify-content-between align-items-center">
 | 
					      <div class="d-flex justify-content-between align-items-center">
 | 
				
			||||||
        <div class="d-flex">
 | 
					        <div class="d-flex align-items-center">
 | 
				
			||||||
          @if (draggable) {
 | 
					          @if (draggable) {
 | 
				
			||||||
            <div class="ms-n2 me-1" cdkDragHandle>
 | 
					            <div class="ms-n2 me-1" cdkDragHandle>
 | 
				
			||||||
              <i-bs name="grip-vertical"></i-bs>
 | 
					              <i-bs name="grip-vertical"></i-bs>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          <h6 class="card-title mb-0">{{title}}</h6>
 | 
					          <h6 class="card-title mb-0">{{title}}</h6>
 | 
				
			||||||
 | 
					          @if (badge) {
 | 
				
			||||||
 | 
					            <span class="badge bg-info text-dark ms-2">{{badge}}</span>
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        @if (loading) {
 | 
					        @if (loading) {
 | 
				
			||||||
          <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
 | 
					          <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,9 @@ export class WidgetFrameComponent
 | 
				
			|||||||
  @Input()
 | 
					  @Input()
 | 
				
			||||||
  cardless: boolean = false
 | 
					  cardless: boolean = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  badge: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngAfterViewInit(): void {
 | 
					  ngAfterViewInit(): void {
 | 
				
			||||||
    setTimeout(() => {
 | 
					    setTimeout(() => {
 | 
				
			||||||
      this.show = true
 | 
					      this.show = true
 | 
				
			||||||
 | 
				
			|||||||
@ -73,6 +73,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
 | 
				
			|||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
 | 
					import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
 | 
				
			||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
 | 
					import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
 | 
				
			||||||
import { DocumentService } from 'src/app/services/rest/document.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 { StoragePathService } from 'src/app/services/rest/storage-path.service'
 | 
				
			||||||
import { UserService } from 'src/app/services/rest/user.service'
 | 
					import { UserService } from 'src/app/services/rest/user.service'
 | 
				
			||||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
					import { SettingsService } from 'src/app/services/settings.service'
 | 
				
			||||||
@ -195,6 +196,7 @@ export class DocumentDetailComponent
 | 
				
			|||||||
  private hotKeyService = inject(HotKeyService)
 | 
					  private hotKeyService = inject(HotKeyService)
 | 
				
			||||||
  private componentRouterService = inject(ComponentRouterService)
 | 
					  private componentRouterService = inject(ComponentRouterService)
 | 
				
			||||||
  private deviceDetectorService = inject(DeviceDetectorService)
 | 
					  private deviceDetectorService = inject(DeviceDetectorService)
 | 
				
			||||||
 | 
					  private savedViewService = inject(SavedViewService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ViewChild('inputTitle')
 | 
					  @ViewChild('inputTitle')
 | 
				
			||||||
  titleInput: TextComponent
 | 
					  titleInput: TextComponent
 | 
				
			||||||
@ -841,6 +843,7 @@ export class DocumentDetailComponent
 | 
				
			|||||||
          } else {
 | 
					          } else {
 | 
				
			||||||
            this.openDocumentService.refreshDocument(this.documentId)
 | 
					            this.openDocumentService.refreshDocument(this.documentId)
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					          this.savedViewService.maybeRefreshDocumentCounts()
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        error: (error) => {
 | 
					        error: (error) => {
 | 
				
			||||||
          this.networkActive = false
 | 
					          this.networkActive = false
 | 
				
			||||||
@ -1188,6 +1191,7 @@ export class DocumentDetailComponent
 | 
				
			|||||||
  notesUpdated(notes: DocumentNote[]) {
 | 
					  notesUpdated(notes: DocumentNote[]) {
 | 
				
			||||||
    this.document.notes = notes
 | 
					    this.document.notes = notes
 | 
				
			||||||
    this.openDocumentService.refreshDocument(this.documentId)
 | 
					    this.openDocumentService.refreshDocument(this.documentId)
 | 
				
			||||||
 | 
					    this.savedViewService.maybeRefreshDocumentCounts()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get userIsOwner(): boolean {
 | 
					  get userIsOwner(): boolean {
 | 
				
			||||||
 | 
				
			|||||||
@ -32,6 +32,7 @@ import {
 | 
				
			|||||||
  DocumentService,
 | 
					  DocumentService,
 | 
				
			||||||
  SelectionDataItem,
 | 
					  SelectionDataItem,
 | 
				
			||||||
} from 'src/app/services/rest/document.service'
 | 
					} 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 { StoragePathService } from 'src/app/services/rest/storage-path.service'
 | 
				
			||||||
import { TagService } from 'src/app/services/rest/tag.service'
 | 
					import { TagService } from 'src/app/services/rest/tag.service'
 | 
				
			||||||
import { SettingsService } from 'src/app/services/settings.service'
 | 
					import { SettingsService } from 'src/app/services/settings.service'
 | 
				
			||||||
@ -83,6 +84,7 @@ export class BulkEditorComponent
 | 
				
			|||||||
  private storagePathService = inject(StoragePathService)
 | 
					  private storagePathService = inject(StoragePathService)
 | 
				
			||||||
  private customFieldService = inject(CustomFieldsService)
 | 
					  private customFieldService = inject(CustomFieldsService)
 | 
				
			||||||
  private permissionService = inject(PermissionsService)
 | 
					  private permissionService = inject(PermissionsService)
 | 
				
			||||||
 | 
					  private savedViewService = inject(SavedViewService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  tagSelectionModel = new FilterableDropdownSelectionModel(true)
 | 
					  tagSelectionModel = new FilterableDropdownSelectionModel(true)
 | 
				
			||||||
  correspondentSelectionModel = new FilterableDropdownSelectionModel()
 | 
					  correspondentSelectionModel = new FilterableDropdownSelectionModel()
 | 
				
			||||||
@ -270,6 +272,7 @@ export class BulkEditorComponent
 | 
				
			|||||||
          this.list.selected.forEach((id) => {
 | 
					          this.list.selected.forEach((id) => {
 | 
				
			||||||
            this.openDocumentService.refreshDocument(id)
 | 
					            this.openDocumentService.refreshDocument(id)
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
 | 
					          this.savedViewService.maybeRefreshDocumentCounts()
 | 
				
			||||||
          if (modal) {
 | 
					          if (modal) {
 | 
				
			||||||
            modal.close()
 | 
					            modal.close()
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
				
			|||||||
@ -58,6 +58,8 @@ export const SETTINGS_KEYS = {
 | 
				
			|||||||
    'general-settings:saved-views:dashboard-views-sort-order',
 | 
					    'general-settings:saved-views:dashboard-views-sort-order',
 | 
				
			||||||
  SIDEBAR_VIEWS_SORT_ORDER:
 | 
					  SIDEBAR_VIEWS_SORT_ORDER:
 | 
				
			||||||
    'general-settings:saved-views: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',
 | 
					  TOUR_COMPLETE: 'general-settings:tour-complete',
 | 
				
			||||||
  DEFAULT_PERMS_OWNER: 'general-settings:permissions:default-owner',
 | 
					  DEFAULT_PERMS_OWNER: 'general-settings:permissions:default-owner',
 | 
				
			||||||
  DEFAULT_PERMS_VIEW_USERS: 'general-settings:permissions:default-view-users',
 | 
					  DEFAULT_PERMS_VIEW_USERS: 'general-settings:permissions:default-view-users',
 | 
				
			||||||
@ -227,6 +229,11 @@ export const SETTINGS: UiSetting[] = [
 | 
				
			|||||||
    type: 'array',
 | 
					    type: 'array',
 | 
				
			||||||
    default: [],
 | 
					    default: [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    key: SETTINGS_KEYS.SIDEBAR_VIEWS_SHOW_COUNT,
 | 
				
			||||||
 | 
					    type: 'boolean',
 | 
				
			||||||
 | 
					    default: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    key: SETTINGS_KEYS.APP_LOGO,
 | 
					    key: SETTINGS_KEYS.APP_LOGO,
 | 
				
			||||||
    type: 'string',
 | 
					    type: 'string',
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ const saved_views = [
 | 
				
			|||||||
    id: 1,
 | 
					    id: 1,
 | 
				
			||||||
    show_on_dashboard: true,
 | 
					    show_on_dashboard: true,
 | 
				
			||||||
    show_in_sidebar: true,
 | 
					    show_in_sidebar: true,
 | 
				
			||||||
    sort_field: 'name',
 | 
					    sort_field: 'title',
 | 
				
			||||||
    sort_reverse: true,
 | 
					    sort_reverse: true,
 | 
				
			||||||
    filter_rules: [],
 | 
					    filter_rules: [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@ -26,7 +26,7 @@ const saved_views = [
 | 
				
			|||||||
    id: 2,
 | 
					    id: 2,
 | 
				
			||||||
    show_on_dashboard: true,
 | 
					    show_on_dashboard: true,
 | 
				
			||||||
    show_in_sidebar: true,
 | 
					    show_in_sidebar: true,
 | 
				
			||||||
    sort_field: 'name',
 | 
					    sort_field: 'created',
 | 
				
			||||||
    sort_reverse: true,
 | 
					    sort_reverse: true,
 | 
				
			||||||
    filter_rules: [],
 | 
					    filter_rules: [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@ -35,7 +35,7 @@ const saved_views = [
 | 
				
			|||||||
    id: 3,
 | 
					    id: 3,
 | 
				
			||||||
    show_on_dashboard: true,
 | 
					    show_on_dashboard: true,
 | 
				
			||||||
    show_in_sidebar: true,
 | 
					    show_in_sidebar: true,
 | 
				
			||||||
    sort_field: 'name',
 | 
					    sort_field: 'added',
 | 
				
			||||||
    sort_reverse: true,
 | 
					    sort_reverse: true,
 | 
				
			||||||
    filter_rules: [],
 | 
					    filter_rules: [],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@ -44,7 +44,7 @@ const saved_views = [
 | 
				
			|||||||
    id: 4,
 | 
					    id: 4,
 | 
				
			||||||
    show_on_dashboard: false,
 | 
					    show_on_dashboard: false,
 | 
				
			||||||
    show_in_sidebar: false,
 | 
					    show_in_sidebar: false,
 | 
				
			||||||
    sort_field: 'name',
 | 
					    sort_field: 'owner',
 | 
				
			||||||
    sort_reverse: true,
 | 
					    sort_reverse: true,
 | 
				
			||||||
    filter_rules: [],
 | 
					    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(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
    // Dont need to setup again
 | 
					    // Dont need to setup again
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,13 @@
 | 
				
			|||||||
import { HttpClient } from '@angular/common/http'
 | 
					import { HttpClient } from '@angular/common/http'
 | 
				
			||||||
import { inject, Injectable } from '@angular/core'
 | 
					import { inject, Injectable } from '@angular/core'
 | 
				
			||||||
import { combineLatest, Observable } from 'rxjs'
 | 
					import { combineLatest, Observable, Subject } from 'rxjs'
 | 
				
			||||||
import { tap } from 'rxjs/operators'
 | 
					import { takeUntil, tap } from 'rxjs/operators'
 | 
				
			||||||
import { Results } from 'src/app/data/results'
 | 
					import { Results } from 'src/app/data/results'
 | 
				
			||||||
import { SavedView } from 'src/app/data/saved-view'
 | 
					import { SavedView } from 'src/app/data/saved-view'
 | 
				
			||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
 | 
					import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
 | 
				
			||||||
import { SettingsService } from '../settings.service'
 | 
					import { SettingsService } from '../settings.service'
 | 
				
			||||||
import { AbstractPaperlessService } from './abstract-paperless-service'
 | 
					import { AbstractPaperlessService } from './abstract-paperless-service'
 | 
				
			||||||
 | 
					import { DocumentService } from './document.service'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable({
 | 
					@Injectable({
 | 
				
			||||||
  providedIn: 'root',
 | 
					  providedIn: 'root',
 | 
				
			||||||
@ -14,9 +15,12 @@ import { AbstractPaperlessService } from './abstract-paperless-service'
 | 
				
			|||||||
export class SavedViewService extends AbstractPaperlessService<SavedView> {
 | 
					export class SavedViewService extends AbstractPaperlessService<SavedView> {
 | 
				
			||||||
  protected http: HttpClient
 | 
					  protected http: HttpClient
 | 
				
			||||||
  private settingsService = inject(SettingsService)
 | 
					  private settingsService = inject(SettingsService)
 | 
				
			||||||
 | 
					  private documentService = inject(DocumentService)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public loading: boolean = true
 | 
					  public loading: boolean = true
 | 
				
			||||||
  private savedViews: SavedView[] = []
 | 
					  private savedViews: SavedView[] = []
 | 
				
			||||||
 | 
					  private savedViewDocumentCounts: Map<number, number> = new Map()
 | 
				
			||||||
 | 
					  private unsubscribeNotifier: Subject<void> = new Subject<void>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    super()
 | 
					    super()
 | 
				
			||||||
@ -46,8 +50,16 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public reload() {
 | 
					  public reload(callback: any = null) {
 | 
				
			||||||
    this.listAll().subscribe()
 | 
					    this.listAll()
 | 
				
			||||||
 | 
					      .pipe(
 | 
				
			||||||
 | 
					        tap((r) => {
 | 
				
			||||||
 | 
					          if (callback) {
 | 
				
			||||||
 | 
					            callback(r)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .subscribe()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get allViews() {
 | 
					  get allViews() {
 | 
				
			||||||
@ -110,4 +122,30 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
 | 
				
			|||||||
  delete(o: SavedView) {
 | 
					  delete(o: SavedView) {
 | 
				
			||||||
    return super.delete(o).pipe(tap(() => this.reload()))
 | 
					    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)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user