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 { Subject, of, throwError } from 'rxjs'
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 { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { PermissionsService } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.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 { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
import { CheckComponent } from '../../common/input/check/check.component'
@@ -31,6 +33,7 @@ describe('SavedViewsComponent', () => {
let component: SavedViewsComponent
let fixture: ComponentFixture<SavedViewsComponent>
let savedViewService: SavedViewService
let settingsService: SettingsService
let toastService: ToastService
let modalService: NgbModal
@@ -79,6 +82,7 @@ describe('SavedViewsComponent', () => {
}).compileComponents()
savedViewService = TestBed.inject(SavedViewService)
settingsService = TestBed.inject(SettingsService)
toastService = TestBed.inject(ToastService)
modalService = TestBed.inject(NgbModal)
fixture = TestBed.createComponent(SavedViewsComponent)
@@ -97,13 +101,12 @@ describe('SavedViewsComponent', () => {
it('should support save saved views, show error', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
const control = component.savedViewsForm
.get('savedViews')
.get(savedViews[0].id.toString())
.get('show_on_dashboard')
control.setValue(!savedViews[0].show_on_dashboard)
.get('name')
control.setValue(`${savedViews[0].name}-changed`)
control.markAsDirty()
// saved views error first
@@ -113,13 +116,12 @@ describe('SavedViewsComponent', () => {
component.save()
expect(toastErrorSpy).toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
toastSpy.mockClear()
toastErrorSpy.mockClear()
savedViewPatchSpy.mockClear()
// succeed saved views
savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[]))
control.setValue(savedViews[0].show_on_dashboard)
control.setValue(savedViews[0].name)
control.markAsDirty()
component.save()
expect(toastErrorSpy).not.toHaveBeenCalled()
@@ -135,24 +137,52 @@ describe('SavedViewsComponent', () => {
component.savedViewsForm
.get('savedViews')
.get(view.id.toString())
.get('show_on_dashboard')
.setValue(!view.show_on_dashboard)
.get('name')
.setValue('changed-view-name')
component.savedViewsForm
.get('savedViews')
.get(view.id.toString())
.get('show_on_dashboard')
.get('name')
.markAsDirty()
fixture.detectChanges()
component.save()
expect(patchSpy).toHaveBeenCalledWith([
expect.objectContaining({
id: view.id,
name: view.name,
show_in_sidebar: view.show_in_sidebar,
show_on_dashboard: !view.show_on_dashboard,
}),
])
expect(patchSpy).toHaveBeenCalled()
const patchBody = patchSpy.mock.calls[0][0][0]
expect(patchBody).toMatchObject({
id: view.id,
name: 'changed-view-name',
})
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', () => {

View File

@@ -8,10 +8,11 @@ import {
} from '@angular/forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
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 { DisplayMode } from 'src/app/data/document'
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 {
PermissionAction,
@@ -107,9 +108,9 @@ export class SavedViewsComponent
name: new FormControl({ value: null, disabled: !canEdit }),
show_on_dashboard: new FormControl({
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 }),
display_mode: new FormControl({ value: null, disabled: !canEdit }),
display_fields: new FormControl({ value: [], disabled: !canEdit }),
@@ -153,27 +154,80 @@ export class SavedViewsComponent
}
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[] = []
Object.values(this.savedViewsGroup.controls)
.filter((g: FormGroup) => g.enabled && !g.pristine)
.forEach((group: FormGroup) => {
changed.push(group.getRawValue())
})
if (changed.length) {
this.savedViewService.patchMany(changed).subscribe({
next: () => {
this.toastService.showInfo($localize`Views saved successfully.`)
this.reloadViews()
},
error: (error) => {
this.toastService.showError(
$localize`Error while saving views.`,
error
)
},
})
const dashboardVisibleIds: number[] = []
const sidebarVisibleIds: number[] = []
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) {
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: () => {
this.toastService.showInfo($localize`Views saved successfully.`)
this.savedViewService.clearCache()
this.reloadViews()
},
error: (error) => {
this.toastService.showError($localize`Error while saving views.`, error)
},
})
}
public canEditSavedView(view: SavedView): boolean {
@@ -223,7 +277,7 @@ export class SavedViewsComponent
private reloadViews(): void {
this.loading = true
this.savedViewService
.listAll(null, null, { full_perms: true })
.list(1, 100000, null, null, { full_perms: true })
.subscribe((r) => {
this.savedViews = r.results
this.initialize()

View File

@@ -36,7 +36,9 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
return super.list(page, pageSize, sortField, sortReverse, extraParams).pipe(
tap({
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.settingsService.dashboardIsEmpty =
this.dashboardViews.length === 0
@@ -70,6 +72,14 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
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 {
const visibleIds = this.getVisibleViewIds(
SETTINGS_KEYS.DASHBOARD_VIEWS_VISIBLE_IDS