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 568b2c184..3cfd9332a 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 @@ -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 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', () => { 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 4ca08657b..ae52c59f8 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 @@ -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() diff --git a/src-ui/src/app/services/rest/saved-view.service.ts b/src-ui/src/app/services/rest/saved-view.service.ts index ba7205e95..e2f7ad13b 100644 --- a/src-ui/src/app/services/rest/saved-view.service.ts +++ b/src-ui/src/app/services/rest/saved-view.service.ts @@ -36,7 +36,9 @@ export class SavedViewService extends AbstractPaperlessService { 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 { 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