mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-24 00:59:35 -06:00
Make views use UISettings visibility instead of model
This commit is contained in:
@@ -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]
|
||||||
id: view.id,
|
expect(patchBody).toMatchObject({
|
||||||
name: view.name,
|
id: view.id,
|
||||||
show_in_sidebar: view.show_in_sidebar,
|
name: 'changed-view-name',
|
||||||
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', () => {
|
||||||
|
|||||||
@@ -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,27 +154,80 @@ 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 (changed.length) {
|
if (value.show_on_dashboard) {
|
||||||
this.savedViewService.patchMany(changed).subscribe({
|
dashboardVisibleIds.push(value.id)
|
||||||
next: () => {
|
}
|
||||||
this.toastService.showInfo($localize`Views saved successfully.`)
|
if (value.show_in_sidebar) {
|
||||||
this.reloadViews()
|
sidebarVisibleIds.push(value.id)
|
||||||
},
|
}
|
||||||
error: (error) => {
|
|
||||||
this.toastService.showError(
|
if (!group.get('name')?.enabled) {
|
||||||
$localize`Error while saving views.`,
|
// Quick check for user doesn't have permissions, then bail
|
||||||
error
|
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 {
|
public canEditSavedView(view: SavedView): boolean {
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user