Make views use UISettings visibility instead of model

This commit is contained in:
shamoon
2026-02-20 11:45:14 -08:00
parent 258777cac9
commit 45456042cb
3 changed files with 134 additions and 40 deletions

View File

@@ -7,11 +7,13 @@ import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject, of, throwError } from 'rxjs' import { Subject, of, throwError } from 'rxjs'
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 { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { PermissionsService } from 'src/app/services/permissions.service' import { PermissionsService } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { CheckComponent } from '../../common/input/check/check.component' import { CheckComponent } from '../../common/input/check/check.component'
@@ -31,6 +33,7 @@ describe('SavedViewsComponent', () => {
let component: SavedViewsComponent let component: SavedViewsComponent
let fixture: ComponentFixture<SavedViewsComponent> let fixture: ComponentFixture<SavedViewsComponent>
let savedViewService: SavedViewService let savedViewService: SavedViewService
let settingsService: SettingsService
let toastService: ToastService let toastService: ToastService
let modalService: NgbModal let modalService: NgbModal
@@ -79,6 +82,7 @@ describe('SavedViewsComponent', () => {
}).compileComponents() }).compileComponents()
savedViewService = TestBed.inject(SavedViewService) savedViewService = TestBed.inject(SavedViewService)
settingsService = TestBed.inject(SettingsService)
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)
modalService = TestBed.inject(NgbModal) modalService = TestBed.inject(NgbModal)
fixture = TestBed.createComponent(SavedViewsComponent) fixture = TestBed.createComponent(SavedViewsComponent)
@@ -97,13 +101,12 @@ describe('SavedViewsComponent', () => {
it('should support save saved views, show error', () => { it('should support save saved views, show error', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError') const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany') const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
const control = component.savedViewsForm const control = component.savedViewsForm
.get('savedViews') .get('savedViews')
.get(savedViews[0].id.toString()) .get(savedViews[0].id.toString())
.get('show_on_dashboard') .get('name')
control.setValue(!savedViews[0].show_on_dashboard) control.setValue(`${savedViews[0].name}-changed`)
control.markAsDirty() control.markAsDirty()
// saved views error first // saved views error first
@@ -113,13 +116,12 @@ describe('SavedViewsComponent', () => {
component.save() component.save()
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled() expect(savedViewPatchSpy).toHaveBeenCalled()
toastSpy.mockClear()
toastErrorSpy.mockClear() toastErrorSpy.mockClear()
savedViewPatchSpy.mockClear() savedViewPatchSpy.mockClear()
// succeed saved views // succeed saved views
savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[])) savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[]))
control.setValue(savedViews[0].show_on_dashboard) control.setValue(savedViews[0].name)
control.markAsDirty() control.markAsDirty()
component.save() component.save()
expect(toastErrorSpy).not.toHaveBeenCalled() expect(toastErrorSpy).not.toHaveBeenCalled()
@@ -135,24 +137,52 @@ describe('SavedViewsComponent', () => {
component.savedViewsForm component.savedViewsForm
.get('savedViews') .get('savedViews')
.get(view.id.toString()) .get(view.id.toString())
.get('show_on_dashboard') .get('name')
.setValue(!view.show_on_dashboard) .setValue('changed-view-name')
component.savedViewsForm component.savedViewsForm
.get('savedViews') .get('savedViews')
.get(view.id.toString()) .get(view.id.toString())
.get('show_on_dashboard') .get('name')
.markAsDirty() .markAsDirty()
fixture.detectChanges() fixture.detectChanges()
component.save() component.save()
expect(patchSpy).toHaveBeenCalledWith([ expect(patchSpy).toHaveBeenCalled()
expect.objectContaining({ const patchBody = patchSpy.mock.calls[0][0][0]
expect(patchBody).toMatchObject({
id: view.id, id: view.id,
name: view.name, name: 'changed-view-name',
show_in_sidebar: view.show_in_sidebar, })
show_on_dashboard: !view.show_on_dashboard, expect(patchBody.show_on_dashboard).toBeUndefined()
}), expect(patchBody.show_in_sidebar).toBeUndefined()
]) })
it('should persist visibility changes to user settings', () => {
const patchSpy = jest.spyOn(savedViewService, 'patchMany')
const setSpy = jest.spyOn(settingsService, 'set')
const storeSpy = jest
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(of({ success: true }))
const dashboardControl = component.savedViewsForm
.get('savedViews')
.get(savedViews[0].id.toString())
.get('show_on_dashboard')
dashboardControl.setValue(false)
dashboardControl.markAsDirty()
component.save()
expect(patchSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS,
[]
)
expect(setSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS,
[savedViews[0].id]
)
expect(storeSpy).toHaveBeenCalled()
}) })
it('should support delete saved view', () => { it('should support delete saved view', () => {

View File

@@ -8,10 +8,11 @@ import {
} from '@angular/forms' } from '@angular/forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { dirtyCheck } from '@ngneat/dirty-check-forms' import { dirtyCheck } from '@ngneat/dirty-check-forms'
import { BehaviorSubject, Observable, takeUntil } from 'rxjs' import { BehaviorSubject, Observable, of, switchMap, takeUntil } from 'rxjs'
import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component' import { PermissionsDialogComponent } from 'src/app/components/common/permissions-dialog/permissions-dialog.component'
import { DisplayMode } from 'src/app/data/document' import { DisplayMode } from 'src/app/data/document'
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 { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { import {
PermissionAction, PermissionAction,
@@ -107,9 +108,9 @@ export class SavedViewsComponent
name: new FormControl({ value: null, disabled: !canEdit }), name: new FormControl({ value: null, disabled: !canEdit }),
show_on_dashboard: new FormControl({ show_on_dashboard: new FormControl({
value: null, value: null,
disabled: !canEdit, disabled: false,
}), }),
show_in_sidebar: new FormControl({ value: null, disabled: !canEdit }), show_in_sidebar: new FormControl({ value: null, disabled: false }),
page_size: new FormControl({ value: null, disabled: !canEdit }), page_size: new FormControl({ value: null, disabled: !canEdit }),
display_mode: new FormControl({ value: null, disabled: !canEdit }), display_mode: new FormControl({ value: null, disabled: !canEdit }),
display_fields: new FormControl({ value: [], disabled: !canEdit }), display_fields: new FormControl({ value: [], disabled: !canEdit }),
@@ -153,28 +154,81 @@ export class SavedViewsComponent
} }
public save() { public save() {
// only patch views that have actually changed // Save only changed views, then save the visibility changes into user settings.
const groups = Object.values(this.savedViewsGroup.controls) as FormGroup[]
const visibilityChanged = groups.some(
(group) =>
group.get('show_on_dashboard')?.dirty ||
group.get('show_in_sidebar')?.dirty
)
const changed: SavedView[] = [] const changed: SavedView[] = []
Object.values(this.savedViewsGroup.controls) const dashboardVisibleIds: number[] = []
.filter((g: FormGroup) => g.enabled && !g.pristine) const sidebarVisibleIds: number[] = []
.forEach((group: FormGroup) => {
changed.push(group.getRawValue()) groups.forEach((group) => {
const value = group.getRawValue()
if (value.show_on_dashboard) {
dashboardVisibleIds.push(value.id)
}
if (value.show_in_sidebar) {
sidebarVisibleIds.push(value.id)
}
if (!group.get('name')?.enabled) {
// Quick check for user doesn't have permissions, then bail
return
}
const modelFieldsChanged =
group.get('name')?.dirty ||
group.get('page_size')?.dirty ||
group.get('display_mode')?.dirty ||
group.get('display_fields')?.dirty
if (!modelFieldsChanged) {
return
}
delete value.show_on_dashboard
delete value.show_in_sidebar
changed.push(value)
}) })
if (!changed.length && !visibilityChanged) {
return
}
// First save only changed views
let saveOperation = of([])
if (changed.length) { if (changed.length) {
this.savedViewService.patchMany(changed).subscribe({ saveOperation = saveOperation.pipe(
switchMap(() => this.savedViewService.patchMany(changed))
)
}
// Then save the visibility changes in the settings
if (visibilityChanged) {
this.settings.set(SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS, [
...new Set(dashboardVisibleIds),
])
this.settings.set(SETTINGS_KEYS.SIDEBAR_VIEWS_VISIBLE_IDS, [
...new Set(sidebarVisibleIds),
])
saveOperation = saveOperation.pipe(
switchMap(() => this.settings.storeSettings())
)
}
saveOperation.subscribe({
next: () => { next: () => {
this.toastService.showInfo($localize`Views saved successfully.`) this.toastService.showInfo($localize`Views saved successfully.`)
this.savedViewService.clearCache()
this.reloadViews() this.reloadViews()
}, },
error: (error) => { error: (error) => {
this.toastService.showError( this.toastService.showError($localize`Error while saving views.`, error)
$localize`Error while saving views.`,
error
)
}, },
}) })
} }
}
public canEditSavedView(view: SavedView): boolean { public canEditSavedView(view: SavedView): boolean {
return this.permissionsService.currentUserHasObjectPermissions( return this.permissionsService.currentUserHasObjectPermissions(
@@ -223,7 +277,7 @@ export class SavedViewsComponent
private reloadViews(): void { private reloadViews(): void {
this.loading = true this.loading = true
this.savedViewService this.savedViewService
.listAll(null, null, { full_perms: true }) .list(1, 100000, null, null, { full_perms: true })
.subscribe((r) => { .subscribe((r) => {
this.savedViews = r.results this.savedViews = r.results
this.initialize() this.initialize()

View File

@@ -36,7 +36,9 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
return super.list(page, pageSize, sortField, sortReverse, extraParams).pipe( return super.list(page, pageSize, sortField, sortReverse, extraParams).pipe(
tap({ tap({
next: (r) => { next: (r) => {
this.savedViews = r.results const views = r.results.map((view) => this.withUserVisibility(view))
this.savedViews = views
r.results = views
this._loading = false this._loading = false
this.settingsService.dashboardIsEmpty = this.settingsService.dashboardIsEmpty =
this.dashboardViews.length === 0 this.dashboardViews.length === 0
@@ -70,6 +72,14 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
return Array.isArray(configured) ? configured : null return Array.isArray(configured) ? configured : null
} }
private withUserVisibility(view: SavedView): SavedView {
return {
...view,
show_on_dashboard: this.isDashboardVisible(view),
show_in_sidebar: this.isSidebarVisible(view),
}
}
private isDashboardVisible(view: SavedView): boolean { private isDashboardVisible(view: SavedView): boolean {
const visibleIds = this.getVisibleViewIds( const visibleIds = this.getVisibleViewIds(
SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS