Support drag + drop reorder of dashboard saved views

This commit is contained in:
shamoon 2023-09-24 01:55:24 -07:00
parent d8314500a0
commit 96eea42a8e
19 changed files with 301 additions and 34 deletions

View File

@ -27,6 +27,7 @@
"ngx-clipboard": "^16.0.0", "ngx-clipboard": "^16.0.0",
"ngx-color": "^9.0.0", "ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1", "ngx-cookie-service": "^16.0.1",
"ngx-drag-drop": "^16.1.0",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.4", "ngx-ui-tour-ng-bootstrap": "^13.0.4",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@ -14061,6 +14062,18 @@
"@angular/core": "^16.0.0" "@angular/core": "^16.0.0"
} }
}, },
"node_modules/ngx-drag-drop": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/ngx-drag-drop/-/ngx-drag-drop-16.1.0.tgz",
"integrity": "sha512-y2l9pJGD7OupsIRkCElN/JqTgzjg2V9ZxymKGQR7ZjjcdjaP1wKkiFWIgVEvLNtb8wgm10U+9tkGwLClGaHkQA==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^16.0.0",
"@angular/core": "^16.0.0"
}
},
"node_modules/ngx-file-drop": { "node_modules/ngx-file-drop": {
"version": "16.0.0", "version": "16.0.0",
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-16.0.0.tgz", "resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-16.0.0.tgz",

View File

@ -29,6 +29,7 @@
"ngx-clipboard": "^16.0.0", "ngx-clipboard": "^16.0.0",
"ngx-color": "^9.0.0", "ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1", "ngx-cookie-service": "^16.0.1",
"ngx-drag-drop": "^16.1.0",
"ngx-file-drop": "^16.0.0", "ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.4", "ngx-ui-tour-ng-bootstrap": "^13.0.4",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",

View File

@ -252,9 +252,12 @@ export class AppComponent implements OnInit, OnDestroy {
} }
public get dragDropEnabled(): boolean { public get dragDropEnabled(): boolean {
return this.permissionsService.currentUserCan( return (
PermissionAction.Add, this.settings.globalDropzoneEnabled &&
PermissionType.Document this.permissionsService.currentUserCan(
PermissionAction.Add,
PermissionType.Document
)
) )
} }

View File

@ -99,6 +99,7 @@ import { ConsumptionTemplatesComponent } from './components/manage/consumption-t
import { ConsumptionTemplateEditDialogComponent } from './components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component' import { ConsumptionTemplateEditDialogComponent } from './components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component'
import { MailComponent } from './components/manage/mail/mail.component' import { MailComponent } from './components/manage/mail/mail.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component' import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { DndModule } from 'ngx-drag-drop'
import localeAf from '@angular/common/locales/af' import localeAf from '@angular/common/locales/af'
import localeAr from '@angular/common/locales/ar' import localeAr from '@angular/common/locales/ar'
@ -254,6 +255,7 @@ function initializeApp(settings: SettingsService) {
NgSelectModule, NgSelectModule,
ColorSliderModule, ColorSliderModule,
TourNgBootstrapModule, TourNgBootstrapModule,
DndModule,
], ],
providers: [ providers: [
{ {

View File

@ -4,7 +4,11 @@
<div class="row"> <div class="row">
<div class="col-auto col-lg-8 col-xl-9 mb-4"> <div class="col-auto col-lg-8 col-xl-9 mb-4">
<div class="row row-cols-1 g-4" tourAnchor="tour.dashboard"> <div class="row row-cols-1 g-4" tourAnchor="tour.dashboard"
dndDropzone
dndEffectAllowed="move"
(dndDrop)="onDrop($event)"
>
<div *ngIf="savedViewService.loading" class="col"> <div *ngIf="savedViewService.loading" class="col">
<div class="spinner-border spinner-border-sm me-2" role="status"></div> <div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container> <ng-container i18n>Loading...</ng-container>
@ -15,12 +19,17 @@
</div> </div>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst"> <div *ngFor="let v of dashboardViews" class="col">
<div class="col"> <pngx-saved-view-widget
<pngx-saved-view-widget [savedView]="v"></pngx-saved-view-widget> [savedView]="v"
</div> (dndStart)="onDragStart($event)"
</ng-container> (dndMoved)="onDragged(v)"
(dndEnd)="onDragEnd($event)"
>
</pngx-saved-view-widget>
</div>
</ng-container> </ng-container>
<div class="p-1" dndPlaceholderRef></div>
</div> </div>
</div> </div>
<div class="col-auto col-lg-4 col-xl-3"> <div class="col-auto col-lg-4 col-xl-3">

View File

@ -17,12 +17,56 @@ import { NgxFileDropModule } from 'ngx-file-drop'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
import { LogoComponent } from '../common/logo/logo.component' import { LogoComponent } from '../common/logo/logo.component'
import { of, throwError } from 'rxjs'
import { DndDropEvent, DndModule } from 'ngx-drag-drop'
import { ToastService } from 'src/app/services/toast.service'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
const saved_views = [
{
name: 'Saved View 0',
id: 0,
show_on_dashboard: true,
show_in_sidebar: true,
sort_field: 'name',
sort_reverse: true,
filter_rules: [],
},
{
name: 'Saved View 1',
id: 1,
show_on_dashboard: false,
show_in_sidebar: false,
sort_field: 'name',
sort_reverse: true,
filter_rules: [],
},
{
name: 'Saved View 2',
id: 2,
show_on_dashboard: true,
show_in_sidebar: false,
sort_field: 'name',
sort_reverse: true,
filter_rules: [],
},
{
name: 'Saved View 3',
id: 3,
show_on_dashboard: true,
show_in_sidebar: false,
sort_field: 'name',
sort_reverse: true,
filter_rules: [],
},
]
describe('DashboardComponent', () => { describe('DashboardComponent', () => {
let component: DashboardComponent let component: DashboardComponent
let fixture: ComponentFixture<DashboardComponent> let fixture: ComponentFixture<DashboardComponent>
let settingsService: SettingsService let settingsService: SettingsService
let tourService: TourService let tourService: TourService
let toastService: ToastService
beforeEach(async () => { beforeEach(async () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -47,24 +91,13 @@ describe('DashboardComponent', () => {
{ {
provide: SavedViewService, provide: SavedViewService,
useValue: { useValue: {
dashboardViews: [ listAll: () =>
{ of({
id: 1, all: [saved_views.map((v) => v.id)],
name: 'saved view 1', count: saved_views.length,
show_on_dashboard: true, results: saved_views,
sort_field: 'added', }),
sort_reverse: true, dashboardViews: saved_views.filter((v) => v.show_on_dashboard),
filter_rules: [],
},
{
id: 2,
name: 'saved view 2',
show_on_dashboard: true,
sort_field: 'created',
sort_reverse: true,
filter_rules: [],
},
],
}, },
}, },
], ],
@ -74,6 +107,7 @@ describe('DashboardComponent', () => {
NgxFileDropModule, NgxFileDropModule,
RouterTestingModule, RouterTestingModule,
TourNgBootstrapModule, TourNgBootstrapModule,
DndModule,
], ],
}).compileComponents() }).compileComponents()
@ -82,7 +116,11 @@ describe('DashboardComponent', () => {
first_name: 'Foo', first_name: 'Foo',
last_name: 'Bar', last_name: 'Bar',
} }
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return [0, 2, 3]
})
tourService = TestBed.inject(TourService) tourService = TestBed.inject(TourService)
toastService = TestBed.inject(ToastService)
fixture = TestBed.createComponent(DashboardComponent) fixture = TestBed.createComponent(DashboardComponent)
component = fixture.componentInstance component = fixture.componentInstance
@ -100,7 +138,7 @@ describe('DashboardComponent', () => {
it('should show dashboard widgets', () => { it('should show dashboard widgets', () => {
expect( expect(
fixture.debugElement.queryAll(By.directive(SavedViewWidgetComponent)) fixture.debugElement.queryAll(By.directive(SavedViewWidgetComponent))
).toHaveLength(2) ).toHaveLength(saved_views.filter((v) => v.show_on_dashboard).length)
}) })
it('should end tour service if still running and welcome widget dismissed', () => { it('should end tour service if still running and welcome widget dismissed', () => {
@ -116,4 +154,44 @@ describe('DashboardComponent', () => {
component.completeTour() component.completeTour()
expect(settingsCompleteTourSpy).toHaveBeenCalled() expect(settingsCompleteTourSpy).toHaveBeenCalled()
}) })
it('should disable global dropzone on start drag + drop, re-enable after', () => {
expect(settingsService.globalDropzoneEnabled).toBeTruthy()
component.onDragStart(null)
expect(settingsService.globalDropzoneEnabled).toBeFalsy()
component.onDragEnd(null)
expect(settingsService.globalDropzoneEnabled).toBeTruthy()
})
it('should update saved view sorting on drag + drop, show info', () => {
const settingsSpy = jest.spyOn(settingsService, 'updateDashboardViewsSort')
const toastSpy = jest.spyOn(toastService, 'showInfo')
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
component.onDrop({ index: 2, data: saved_views[0] } as DndDropEvent)
component.onDragged(saved_views[0])
expect(settingsSpy).toHaveBeenCalledWith([
saved_views[2],
saved_views[0],
saved_views[3],
])
expect(toastSpy).toHaveBeenCalled()
component.onDrop({ data: saved_views[3] } as DndDropEvent)
})
it('should update saved view sorting on drag + drop, show info2', () => {
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
if (key === SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER) return []
})
fixture.destroy()
fixture = TestBed.createComponent(DashboardComponent)
component = fixture.componentInstance
fixture.detectChanges()
const toastSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(settingsService, 'storeSettings')
.mockReturnValue(throwError(() => new Error('unable to save')))
component.onDrop({ index: 2, data: saved_views[0] } as DndDropEvent)
component.onDragged(saved_views[0])
expect(toastSpy).toHaveBeenCalled()
})
}) })

View File

@ -3,6 +3,10 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { TourService } from 'ngx-ui-tour-ng-bootstrap' import { TourService } from 'ngx-ui-tour-ng-bootstrap'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { DndDropEvent } from 'ngx-drag-drop'
import { ToastService } from 'src/app/services/toast.service'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
@Component({ @Component({
selector: 'pngx-dashboard', selector: 'pngx-dashboard',
@ -10,12 +14,38 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap'
styleUrls: ['./dashboard.component.scss'], styleUrls: ['./dashboard.component.scss'],
}) })
export class DashboardComponent extends ComponentWithPermissions { export class DashboardComponent extends ComponentWithPermissions {
public dashboardViews: PaperlessSavedView[] = []
constructor( constructor(
public settingsService: SettingsService, public settingsService: SettingsService,
public savedViewService: SavedViewService, public savedViewService: SavedViewService,
private tourService: TourService private tourService: TourService,
private toastService: ToastService
) { ) {
super() super()
this.savedViewService.listAll().subscribe(() => {
const sorted: number[] = this.settingsService.get(
SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER
)
this.dashboardViews =
sorted?.length > 0
? sorted
.map((id) =>
this.savedViewService.dashboardViews.find((v) => v.id === id)
)
.concat(
this.savedViewService.dashboardViews.filter(
(v) => !sorted.includes(v.id)
)
)
.filter((v) => v)
: [...this.savedViewService.dashboardViews]
console.log(
this.dashboardViews,
sorted,
this.savedViewService.dashboardViews
)
})
} }
get subtitle() { get subtitle() {
@ -33,4 +63,35 @@ export class DashboardComponent extends ComponentWithPermissions {
this.settingsService.completeTour() this.settingsService.completeTour()
} }
} }
onDragStart(event: DragEvent) {
this.settingsService.globalDropzoneEnabled = false
}
onDragged(v: PaperlessSavedView) {
const index = this.dashboardViews.indexOf(v)
this.dashboardViews.splice(index, 1)
this.settingsService
.updateDashboardViewsSort(this.dashboardViews)
.subscribe({
next: () => {
this.toastService.showInfo($localize`Dashboard updated`)
},
error: (e) => {
this.toastService.showError($localize`Error updating dashboard`, e)
},
})
}
onDragEnd(event: DragEvent) {
this.settingsService.globalDropzoneEnabled = true
}
onDrop(event: DndDropEvent) {
if (typeof event.index === 'undefined') {
event.index = this.dashboardViews.length
}
this.dashboardViews.splice(event.index, 0, event.data)
}
} }

View File

@ -1,4 +1,13 @@
<pngx-widget-frame [title]="savedView.name" [loading]="loading" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"> <pngx-widget-frame
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }"
[title]="savedView.name"
[loading]="loading"
[draggable]="savedView"
(dndStart)="dndStart.emit($event)"
(dndMoved)="dndMoved.emit($event)"
(dndCanceled)="dndCanceled.emit($event)"
(dndEnd)="dndEnd.emit($event)"
>
<a *ngIf="documents.length" class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a> <a *ngIf="documents.length" class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>

View File

@ -28,6 +28,7 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
import { SavedViewWidgetComponent } from './saved-view-widget.component' import { SavedViewWidgetComponent } from './saved-view-widget.component'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe' import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { DndModule } from 'ngx-drag-drop'
const savedView: PaperlessSavedView = { const savedView: PaperlessSavedView = {
id: 1, id: 1,
@ -90,6 +91,7 @@ describe('SavedViewWidgetComponent', () => {
HttpClientTestingModule, HttpClientTestingModule,
NgbModule, NgbModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
DndModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,8 +1,10 @@
import { import {
Component, Component,
EventEmitter,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output,
QueryList, QueryList,
ViewChildren, ViewChildren,
} from '@angular/core' } from '@angular/core'
@ -51,6 +53,18 @@ export class SavedViewWidgetComponent
@Input() @Input()
savedView: PaperlessSavedView savedView: PaperlessSavedView
@Output()
dndStart: EventEmitter<DragEvent> = new EventEmitter()
@Output()
dndMoved: EventEmitter<DragEvent> = new EventEmitter()
@Output()
dndCanceled: EventEmitter<DragEvent> = new EventEmitter()
@Output()
dndEnd: EventEmitter<DragEvent> = new EventEmitter()
documents: PaperlessDocument[] = [] documents: PaperlessDocument[] = []
unsubscribeNotifier: Subject<any> = new Subject() unsubscribeNotifier: Subject<any> = new Subject()

View File

@ -12,6 +12,7 @@ import { RouterTestingModule } from '@angular/router/testing'
import { routes } from 'src/app/app-routing.module' import { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { DndModule } from 'ngx-drag-drop'
describe('StatisticsWidgetComponent', () => { describe('StatisticsWidgetComponent', () => {
let component: StatisticsWidgetComponent let component: StatisticsWidgetComponent
@ -30,6 +31,7 @@ describe('StatisticsWidgetComponent', () => {
HttpClientTestingModule, HttpClientTestingModule,
NgbModule, NgbModule,
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
DndModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -26,6 +26,7 @@ import { PermissionsService } from 'src/app/services/permissions.service'
import { UploadDocumentsService } from 'src/app/services/upload-documents.service' import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component' import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
import { UploadFileWidgetComponent } from './upload-file-widget.component' import { UploadFileWidgetComponent } from './upload-file-widget.component'
import { DndModule } from 'ngx-drag-drop'
describe('UploadFileWidgetComponent', () => { describe('UploadFileWidgetComponent', () => {
let component: UploadFileWidgetComponent let component: UploadFileWidgetComponent
@ -55,6 +56,7 @@ describe('UploadFileWidgetComponent', () => {
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),
NgxFileDropModule, NgxFileDropModule,
NgbAlertModule, NgbAlertModule,
DndModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,7 +1,21 @@
<div class="card shadow-sm bg-light"> <div class="card shadow-sm bg-light"
[dndDraggable]="draggable"
dndEffectAllowed="move"
[dndDisableIf]="!draggable"
(dndStart)="dndStart.emit($event)"
(dndMoved)="dndMoved.emit($event)"
(dndCanceled)="dndCanceled.emit($event)"
(dndEnd)="dndEnd.emit($event)">
<div class="card-header"> <div class="card-header">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h6 class="card-title mb-0">{{title}}</h6> <div class="d-flex">
<div *ngIf="draggable" class="ms-n2 me-1" dndHandle>
<svg class="sidebaricon text-muted" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/>
</svg>
</div>
<h6 class="card-title mb-0">{{title}}</h6>
</div>
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div> <div class="visually-hidden" i18n>Loading...</div>

View File

@ -4,6 +4,7 @@ import { By } from '@angular/platform-browser'
import { NgbAlertModule, NgbAlert } from '@ng-bootstrap/ng-bootstrap' import { NgbAlertModule, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { WidgetFrameComponent } from './widget-frame.component' import { WidgetFrameComponent } from './widget-frame.component'
import { DndModule } from 'ngx-drag-drop'
@Component({ @Component({
template: ` template: `
@ -29,7 +30,7 @@ describe('WidgetFrameComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [WidgetFrameComponent, WidgetFrameComponent], declarations: [WidgetFrameComponent, WidgetFrameComponent],
providers: [PermissionsGuard], providers: [PermissionsGuard],
imports: [NgbAlertModule], imports: [NgbAlertModule, DndModule],
}).compileComponents() }).compileComponents()
fixture = TestBed.createComponent(WidgetFrameComponent) fixture = TestBed.createComponent(WidgetFrameComponent)

View File

@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core' import { Component, EventEmitter, Input, Output } from '@angular/core'
@Component({ @Component({
selector: 'pngx-widget-frame', selector: 'pngx-widget-frame',
@ -13,4 +13,19 @@ export class WidgetFrameComponent {
@Input() @Input()
loading: boolean = false loading: boolean = false
@Input()
draggable: any
@Output()
dndStart: EventEmitter<DragEvent> = new EventEmitter()
@Output()
dndMoved: EventEmitter<DragEvent> = new EventEmitter()
@Output()
dndCanceled: EventEmitter<DragEvent> = new EventEmitter()
@Output()
dndEnd: EventEmitter<DragEvent> = new EventEmitter()
} }

View File

@ -41,6 +41,8 @@ export const SETTINGS_KEYS = {
'general-settings:update-checking:backend-setting', 'general-settings:update-checking:backend-setting',
SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE: SAVED_VIEWS_WARN_ON_UNSAVED_CHANGE:
'general-settings:saved-views:warn-on-unsaved-change', 'general-settings:saved-views:warn-on-unsaved-change',
DASHBOARD_VIEWS_SORT_ORDER:
'general-settings:saved-views:dashboard-views-sort-order',
TOUR_COMPLETE: 'general-settings:tour-complete', TOUR_COMPLETE: 'general-settings:tour-complete',
DEFAULT_PERMS_OWNER: 'general-settings:permissions:default-owner', DEFAULT_PERMS_OWNER: 'general-settings:permissions:default-owner',
DEFAULT_PERMS_VIEW_USERS: 'general-settings:permissions:default-view-users', DEFAULT_PERMS_VIEW_USERS: 'general-settings:permissions:default-view-users',
@ -180,4 +182,9 @@ export const SETTINGS: PaperlessUiSetting[] = [
type: 'array', type: 'array',
default: [], default: [],
}, },
{
key: SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER,
type: 'array',
default: [],
},
] ]

View File

@ -15,6 +15,7 @@ import {
SETTINGS_KEYS, SETTINGS_KEYS,
} from '../data/paperless-uisettings' } from '../data/paperless-uisettings'
import { SettingsService } from './settings.service' import { SettingsService } from './settings.service'
import { PaperlessSavedView } from '../data/paperless-saved-view'
describe('SettingsService', () => { describe('SettingsService', () => {
let httpTestingController: HttpTestingController let httpTestingController: HttpTestingController
@ -277,4 +278,22 @@ describe('SettingsService', () => {
)[0] )[0]
expect(req.request.method).toEqual('POST') expect(req.request.method).toEqual('POST')
}) })
it('should update saved view sorting', () => {
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush(ui_settings)
const setSpy = jest.spyOn(settingsService, 'set')
settingsService.updateDashboardViewsSort([
{ id: 1 } as PaperlessSavedView,
{ id: 4 } as PaperlessSavedView,
])
expect(setSpy).toHaveBeenCalledWith(
SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER,
[1, 4]
)
httpTestingController
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
.flush(ui_settings)
})
}) })

View File

@ -26,6 +26,7 @@ import { PaperlessUser } from '../data/paperless-user'
import { PermissionsService } from './permissions.service' import { PermissionsService } from './permissions.service'
import { SavedViewService } from './rest/saved-view.service' import { SavedViewService } from './rest/saved-view.service'
import { ToastService } from './toast.service' import { ToastService } from './toast.service'
import { PaperlessSavedView } from '../data/paperless-saved-view'
export interface LanguageOption { export interface LanguageOption {
code: string code: string
@ -54,6 +55,8 @@ export class SettingsService {
return this._renderer return this._renderer
} }
public globalDropzoneEnabled: boolean = true
constructor( constructor(
rendererFactory: RendererFactory2, rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document, @Inject(DOCUMENT) private document,
@ -531,4 +534,13 @@ export class SettingsService {
}) })
} }
} }
updateDashboardViewsSort(
dashboardViews: PaperlessSavedView[]
): Observable<any> {
this.set(SETTINGS_KEYS.DASHBOARD_VIEWS_SORT_ORDER, [
...new Set(dashboardViews.map((v) => v.id)),
])
return this.storeSettings()
}
} }