Support drag + drop reorder of dashboard saved views
This commit is contained in:
parent
d8314500a0
commit
96eea42a8e
13
src-ui/package-lock.json
generated
13
src-ui/package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: [
|
||||||
{
|
{
|
||||||
|
@ -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">
|
||||||
|
@ -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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
svg {
|
||||||
|
cursor: move;
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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: [],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user