From e9d87ef04947cd31c1f93f0431bb7763d6b64b0f Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Fri, 20 Feb 2026 08:45:11 -0800
Subject: [PATCH] Basic frontend respect saved view perms
---
.../document-list.component.html | 8 ++--
.../document-list.component.spec.ts | 47 +++++++++++++++++-
.../document-list/document-list.component.ts | 36 ++++++++++++--
.../saved-views/saved-views.component.html | 20 ++++----
.../saved-views/saved-views.component.spec.ts | 48 +++++++++----------
.../saved-views/saved-views.component.ts | 41 ++++++++++++----
6 files changed, 147 insertions(+), 53 deletions(-)
diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html
index 18c4a2fcc..eb0959d44 100644
--- a/src-ui/src/app/components/document-list/document-list.component.html
+++ b/src-ui/src/app/components/document-list/document-list.component.html
@@ -104,11 +104,9 @@
}
}
-
= new Subject()
get savedViewIsModified(): boolean {
- if (!this.list.activeSavedViewId || !this.unmodifiedSavedView) return false
- else {
+ if (
+ !this.list.activeSavedViewId ||
+ !this.unmodifiedSavedView ||
+ !this.activeSavedViewCanChange
+ ) {
+ return false
+ } else {
return (
this.unmodifiedSavedView.sort_field !== this.list.sortField ||
this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse ||
@@ -180,6 +189,16 @@ export class DocumentListComponent
}
}
+ get activeSavedViewCanChange(): boolean {
+ if (!this.activeSavedView) {
+ return false
+ }
+ return this.permissionService.currentUserHasObjectPermissions(
+ PermissionAction.Change,
+ this.activeSavedView
+ )
+ }
+
get isFiltered() {
return !!this.filterEditor?.rulesModified
}
@@ -256,11 +275,13 @@ export class DocumentListComponent
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ view }) => {
if (!view) {
+ this.activeSavedView = null
this.router.navigate(['404'], {
replaceUrl: true,
})
return
}
+ this.activeSavedView = view
this.unmodifiedSavedView = view
this.list.activateSavedViewWithQueryParams(
view,
@@ -284,6 +305,7 @@ export class DocumentListComponent
// loading a saved view on /documents
this.loadViewConfig(parseInt(queryParams.get('view')))
} else {
+ this.activeSavedView = null
this.list.activateSavedView(null)
this.list.loadFromQueryParams(queryParams)
this.unmodifiedFilterRules = []
@@ -366,7 +388,7 @@ export class DocumentListComponent
}
saveViewConfig() {
- if (this.list.activeSavedViewId != null) {
+ if (this.list.activeSavedViewId != null && this.activeSavedViewCanChange) {
let savedView: SavedView = {
id: this.list.activeSavedViewId,
filter_rules: this.list.filterRules,
@@ -380,6 +402,7 @@ export class DocumentListComponent
.pipe(first())
.subscribe({
next: (view) => {
+ this.activeSavedView = view
this.unmodifiedSavedView = view
this.toastService.showInfo(
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
@@ -401,6 +424,11 @@ export class DocumentListComponent
.getCached(viewID)
.pipe(first())
.subscribe((view) => {
+ if (!view) {
+ this.activeSavedView = null
+ return
+ }
+ this.activeSavedView = view
this.unmodifiedSavedView = view
this.list.activateSavedView(view)
this.list.reload(() => {
diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.html b/src-ui/src/app/components/manage/saved-views/saved-views.component.html
index 2f5ef3338..9f7b5be7f 100644
--- a/src-ui/src/app/components/manage/saved-views/saved-views.component.html
+++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.html
@@ -25,15 +25,17 @@
-
-
-
+ @if (canDeleteSavedView(view)) {
+
+
+
+ }
diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts b/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts
index 10bc5db8e..4f44bf3d0 100644
--- a/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts
+++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.spec.ts
@@ -3,7 +3,6 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
-import { By } from '@angular/platform-browser'
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
@@ -57,6 +56,8 @@ describe('SavedViewsComponent', () => {
provide: PermissionsService,
useValue: {
currentUserCan: () => true,
+ currentUserHasObjectPermissions: () => true,
+ currentUserOwnsObject: () => true,
},
},
{
@@ -96,12 +97,11 @@ describe('SavedViewsComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'show')
const savedViewPatchSpy = jest.spyOn(savedViewService, 'patchMany')
-
- const toggle = fixture.debugElement.query(
- By.css('.form-check.form-switch input')
- )
- toggle.nativeElement.checked = true
- toggle.nativeElement.dispatchEvent(new Event('change'))
+ const control = component.savedViewsForm
+ .get('savedViews')
+ .get(savedViews[0].id.toString())
+ .get('show_on_dashboard')
+ control.setValue(!savedViews[0].show_on_dashboard)
// saved views error first
savedViewPatchSpy.mockReturnValueOnce(
@@ -116,6 +116,7 @@ describe('SavedViewsComponent', () => {
// succeed saved views
savedViewPatchSpy.mockReturnValueOnce(of(savedViews as SavedView[]))
+ control.setValue(savedViews[0].show_on_dashboard)
component.save()
expect(toastErrorSpy).not.toHaveBeenCalled()
expect(savedViewPatchSpy).toHaveBeenCalled()
@@ -127,25 +128,21 @@ describe('SavedViewsComponent', () => {
expect(patchSpy).not.toHaveBeenCalled()
const view = savedViews[0]
- const toggle = fixture.debugElement.query(
- By.css('.form-check.form-switch input')
- )
- toggle.nativeElement.checked = true
- toggle.nativeElement.dispatchEvent(new Event('change'))
- // register change
- component.savedViewsForm.get('savedViews').get(view.id.toString()).value[
- 'show_on_dashboard'
- ] = !view.show_on_dashboard
+ component.savedViewsForm
+ .get('savedViews')
+ .get(view.id.toString())
+ .get('show_on_dashboard')
+ .setValue(!view.show_on_dashboard)
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,
- },
+ }),
])
})
@@ -162,14 +159,17 @@ describe('SavedViewsComponent', () => {
it('should support reset', () => {
const view = savedViews[0]
- component.savedViewsForm.get('savedViews').get(view.id.toString()).value[
- 'show_on_dashboard'
- ] = !view.show_on_dashboard
+ component.savedViewsForm
+ .get('savedViews')
+ .get(view.id.toString())
+ .get('show_on_dashboard')
+ .setValue(!view.show_on_dashboard)
component.reset()
expect(
- component.savedViewsForm.get('savedViews').get(view.id.toString()).value[
- 'show_on_dashboard'
- ]
+ component.savedViewsForm
+ .get('savedViews')
+ .get(view.id.toString())
+ .get('show_on_dashboard').value
).toEqual(view.show_on_dashboard)
})
})
diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.ts b/src-ui/src/app/components/manage/saved-views/saved-views.component.ts
index 015f9b486..765a01ead 100644
--- a/src-ui/src/app/components/manage/saved-views/saved-views.component.ts
+++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.ts
@@ -11,6 +11,10 @@ import { BehaviorSubject, Observable, takeUntil } from 'rxjs'
import { DisplayMode } from 'src/app/data/document'
import { SavedView } from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import {
+ PermissionAction,
+ PermissionsService,
+} from 'src/app/services/permissions.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'
@@ -41,6 +45,7 @@ export class SavedViewsComponent
implements OnInit, OnDestroy
{
private savedViewService = inject(SavedViewService)
+ private permissionsService = inject(PermissionsService)
private settings = inject(SettingsService)
private toastService = inject(ToastService)
@@ -95,16 +100,20 @@ export class SavedViewsComponent
display_mode: view.display_mode,
display_fields: view.display_fields,
}
+ const canEdit = this.canEditSavedView(view)
this.savedViewsGroup.addControl(
view.id.toString(),
new FormGroup({
- id: new FormControl(null),
- name: new FormControl(null),
- show_on_dashboard: new FormControl(null),
- show_in_sidebar: new FormControl(null),
- page_size: new FormControl(null),
- display_mode: new FormControl(null),
- display_fields: new FormControl([]),
+ id: new FormControl({ value: null, disabled: !canEdit }),
+ name: new FormControl({ value: null, disabled: !canEdit }),
+ show_on_dashboard: new FormControl({
+ value: null,
+ disabled: !canEdit,
+ }),
+ show_in_sidebar: new FormControl({ value: null, disabled: !canEdit }),
+ page_size: new FormControl({ value: null, disabled: !canEdit }),
+ display_mode: new FormControl({ value: null, disabled: !canEdit }),
+ display_fields: new FormControl({ value: [], disabled: !canEdit }),
})
)
}
@@ -126,6 +135,9 @@ export class SavedViewsComponent
}
public deleteSavedView(savedView: SavedView) {
+ if (!this.canDeleteSavedView(savedView)) {
+ return
+ }
this.savedViewService.delete(savedView).subscribe(() => {
this.savedViewsGroup.removeControl(savedView.id.toString())
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
@@ -148,9 +160,9 @@ export class SavedViewsComponent
// only patch views that have actually changed
const changed: SavedView[] = []
Object.values(this.savedViewsGroup.controls)
- .filter((g: FormGroup) => !g.pristine)
+ .filter((g: FormGroup) => g.enabled && !g.pristine)
.forEach((group: FormGroup) => {
- changed.push(group.value)
+ changed.push(group.getRawValue())
})
if (changed.length) {
this.savedViewService.patchMany(changed).subscribe({
@@ -167,4 +179,15 @@ export class SavedViewsComponent
})
}
}
+
+ public canEditSavedView(view: SavedView): boolean {
+ return this.permissionsService.currentUserHasObjectPermissions(
+ PermissionAction.Change,
+ view
+ )
+ }
+
+ public canDeleteSavedView(view: SavedView): boolean {
+ return this.permissionsService.currentUserOwnsObject(view)
+ }
}