diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json
index 16d74407e..e64cadc44 100644
--- a/src-ui/package-lock.json
+++ b/src-ui/package-lock.json
@@ -27,6 +27,7 @@
"ngx-clipboard": "^16.0.0",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1",
+ "ngx-drag-drop": "^16.1.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.4",
"rxjs": "^7.8.1",
@@ -14061,6 +14062,18 @@
"@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": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-16.0.0.tgz",
diff --git a/src-ui/package.json b/src-ui/package.json
index d70a59666..2fd5d82ee 100644
--- a/src-ui/package.json
+++ b/src-ui/package.json
@@ -29,6 +29,7 @@
"ngx-clipboard": "^16.0.0",
"ngx-color": "^9.0.0",
"ngx-cookie-service": "^16.0.1",
+ "ngx-drag-drop": "^16.1.0",
"ngx-file-drop": "^16.0.0",
"ngx-ui-tour-ng-bootstrap": "^13.0.4",
"rxjs": "^7.8.1",
diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts
index 0aa9ba210..d0f893da6 100644
--- a/src-ui/src/app/app.component.ts
+++ b/src-ui/src/app/app.component.ts
@@ -252,9 +252,12 @@ export class AppComponent implements OnInit, OnDestroy {
}
public get dragDropEnabled(): boolean {
- return this.permissionsService.currentUserCan(
- PermissionAction.Add,
- PermissionType.Document
+ return (
+ this.settings.globalDropzoneEnabled &&
+ this.permissionsService.currentUserCan(
+ PermissionAction.Add,
+ PermissionType.Document
+ )
)
}
diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts
index 9d9307492..945dfe515 100644
--- a/src-ui/src/app/app.module.ts
+++ b/src-ui/src/app/app.module.ts
@@ -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 { MailComponent } from './components/manage/mail/mail.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 localeAr from '@angular/common/locales/ar'
@@ -254,6 +255,7 @@ function initializeApp(settings: SettingsService) {
NgSelectModule,
ColorSliderModule,
TourNgBootstrapModule,
+ DndModule,
],
providers: [
{
diff --git a/src-ui/src/app/components/dashboard/dashboard.component.html b/src-ui/src/app/components/dashboard/dashboard.component.html
index 492f339a0..7b7116c15 100644
--- a/src-ui/src/app/components/dashboard/dashboard.component.html
+++ b/src-ui/src/app/components/dashboard/dashboard.component.html
@@ -4,7 +4,11 @@
-
+
Loading...
@@ -15,12 +19,17 @@
-
-
-
+
+
diff --git a/src-ui/src/app/components/dashboard/dashboard.component.spec.ts b/src-ui/src/app/components/dashboard/dashboard.component.spec.ts
index 6d100510d..f48340205 100644
--- a/src-ui/src/app/components/dashboard/dashboard.component.spec.ts
+++ b/src-ui/src/app/components/dashboard/dashboard.component.spec.ts
@@ -17,12 +17,56 @@ import { NgxFileDropModule } from 'ngx-file-drop'
import { RouterTestingModule } from '@angular/router/testing'
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
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', () => {
let component: DashboardComponent
let fixture: ComponentFixture
let settingsService: SettingsService
let tourService: TourService
+ let toastService: ToastService
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -47,24 +91,13 @@ describe('DashboardComponent', () => {
{
provide: SavedViewService,
useValue: {
- dashboardViews: [
- {
- id: 1,
- name: 'saved view 1',
- show_on_dashboard: true,
- sort_field: 'added',
- sort_reverse: true,
- filter_rules: [],
- },
- {
- id: 2,
- name: 'saved view 2',
- show_on_dashboard: true,
- sort_field: 'created',
- sort_reverse: true,
- filter_rules: [],
- },
- ],
+ listAll: () =>
+ of({
+ all: [saved_views.map((v) => v.id)],
+ count: saved_views.length,
+ results: saved_views,
+ }),
+ dashboardViews: saved_views.filter((v) => v.show_on_dashboard),
},
},
],
@@ -74,6 +107,7 @@ describe('DashboardComponent', () => {
NgxFileDropModule,
RouterTestingModule,
TourNgBootstrapModule,
+ DndModule,
],
}).compileComponents()
@@ -82,7 +116,11 @@ describe('DashboardComponent', () => {
first_name: 'Foo',
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)
+ toastService = TestBed.inject(ToastService)
fixture = TestBed.createComponent(DashboardComponent)
component = fixture.componentInstance
@@ -100,7 +138,7 @@ describe('DashboardComponent', () => {
it('should show dashboard widgets', () => {
expect(
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', () => {
@@ -116,4 +154,44 @@ describe('DashboardComponent', () => {
component.completeTour()
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()
+ })
})
diff --git a/src-ui/src/app/components/dashboard/dashboard.component.ts b/src-ui/src/app/components/dashboard/dashboard.component.ts
index 8d47495a8..1ac2b9962 100644
--- a/src-ui/src/app/components/dashboard/dashboard.component.ts
+++ b/src-ui/src/app/components/dashboard/dashboard.component.ts
@@ -3,6 +3,10 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
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({
selector: 'pngx-dashboard',
@@ -10,12 +14,38 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap'
styleUrls: ['./dashboard.component.scss'],
})
export class DashboardComponent extends ComponentWithPermissions {
+ public dashboardViews: PaperlessSavedView[] = []
constructor(
public settingsService: SettingsService,
public savedViewService: SavedViewService,
- private tourService: TourService
+ private tourService: TourService,
+ private toastService: ToastService
) {
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() {
@@ -33,4 +63,35 @@ export class DashboardComponent extends ComponentWithPermissions {
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)
+ }
}
diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html
index 4013c5085..fdaa7e9e2 100644
--- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html
+++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html
@@ -1,4 +1,13 @@
-
+
Show all
diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts
index 7b9c5c8b0..c355bdf61 100644
--- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts
+++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts
@@ -28,6 +28,7 @@ import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
import { SavedViewWidgetComponent } from './saved-view-widget.component'
import { By } from '@angular/platform-browser'
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
+import { DndModule } from 'ngx-drag-drop'
const savedView: PaperlessSavedView = {
id: 1,
@@ -90,6 +91,7 @@ describe('SavedViewWidgetComponent', () => {
HttpClientTestingModule,
NgbModule,
RouterTestingModule.withRoutes(routes),
+ DndModule,
],
}).compileComponents()
diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts
index 982aeebaa..f6a5d8c49 100644
--- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts
+++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts
@@ -1,8 +1,10 @@
import {
Component,
+ EventEmitter,
Input,
OnDestroy,
OnInit,
+ Output,
QueryList,
ViewChildren,
} from '@angular/core'
@@ -51,6 +53,18 @@ export class SavedViewWidgetComponent
@Input()
savedView: PaperlessSavedView
+ @Output()
+ dndStart: EventEmitter = new EventEmitter()
+
+ @Output()
+ dndMoved: EventEmitter = new EventEmitter()
+
+ @Output()
+ dndCanceled: EventEmitter = new EventEmitter()
+
+ @Output()
+ dndEnd: EventEmitter = new EventEmitter()
+
documents: PaperlessDocument[] = []
unsubscribeNotifier: Subject = new Subject()
diff --git a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts
index 5bddfaab1..609f0dd7d 100644
--- a/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts
+++ b/src-ui/src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.spec.ts
@@ -12,6 +12,7 @@ import { RouterTestingModule } from '@angular/router/testing'
import { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
+import { DndModule } from 'ngx-drag-drop'
describe('StatisticsWidgetComponent', () => {
let component: StatisticsWidgetComponent
@@ -30,6 +31,7 @@ describe('StatisticsWidgetComponent', () => {
HttpClientTestingModule,
NgbModule,
RouterTestingModule.withRoutes(routes),
+ DndModule,
],
}).compileComponents()
diff --git a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts
index 3afebc7b2..c180af768 100644
--- a/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts
+++ b/src-ui/src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.spec.ts
@@ -26,6 +26,7 @@ import { PermissionsService } from 'src/app/services/permissions.service'
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
import { UploadFileWidgetComponent } from './upload-file-widget.component'
+import { DndModule } from 'ngx-drag-drop'
describe('UploadFileWidgetComponent', () => {
let component: UploadFileWidgetComponent
@@ -55,6 +56,7 @@ describe('UploadFileWidgetComponent', () => {
RouterTestingModule.withRoutes(routes),
NgxFileDropModule,
NgbAlertModule,
+ DndModule,
],
}).compileComponents()
diff --git a/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html b/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html
index 60fa945af..1fb998073 100644
--- a/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html
+++ b/src-ui/src/app/components/dashboard/widgets/widget-frame/widget-frame.component.html
@@ -1,7 +1,21 @@
-