mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: support sorting sidebar saved views (#4381)
This commit is contained in:
@@ -180,11 +180,23 @@ describe('SettingsComponent', () => {
|
||||
activatedRoute.snapshot.fragment = '#notifications'
|
||||
const scrollSpy = jest.spyOn(viewportScroller, 'scrollToAnchor')
|
||||
component.ngOnInit()
|
||||
expect(component.activeNavID).toEqual(3) // Users & Groups
|
||||
expect(component.activeNavID).toEqual(3) // Notifications
|
||||
component.ngAfterViewInit()
|
||||
expect(scrollSpy).toHaveBeenCalledWith('#notifications')
|
||||
})
|
||||
|
||||
it('should enable organizing of sidebar saved views even on direct navigation', () => {
|
||||
completeSetup()
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ section: 'savedviews' })))
|
||||
activatedRoute.snapshot.fragment = '#savedviews'
|
||||
component.ngOnInit()
|
||||
expect(component.activeNavID).toEqual(4) // Saved Views
|
||||
component.ngAfterViewInit()
|
||||
expect(settingsService.organizingSidebarSavedViews).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support save saved views, show error', () => {
|
||||
completeSetup()
|
||||
|
||||
|
@@ -194,6 +194,9 @@ export class SettingsComponent
|
||||
if (navIDKey) {
|
||||
this.activeNavID = SettingsNavIDs[navIDKey]
|
||||
}
|
||||
if (this.activeNavID === SettingsNavIDs.SavedViews) {
|
||||
this.settings.organizingSidebarSavedViews = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -275,11 +278,15 @@ export class SettingsComponent
|
||||
this.router
|
||||
.navigate(['settings', foundNavIDkey.toLowerCase()])
|
||||
.then((navigated) => {
|
||||
this.settings.organizingSidebarSavedViews = false
|
||||
if (!navigated && this.isDirty) {
|
||||
this.activeNavID = navChangeEvent.activeId
|
||||
} else if (navigated && this.isDirty) {
|
||||
this.initialize()
|
||||
}
|
||||
if (this.activeNavID === SettingsNavIDs.SavedViews) {
|
||||
this.settings.organizingSidebarSavedViews = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -352,6 +359,7 @@ export class SettingsComponent
|
||||
ngOnDestroy() {
|
||||
if (this.isDirty) this.settings.updateAppearanceSettings() // in case user changed appearance but didnt save
|
||||
this.storeSub && this.storeSub.unsubscribe()
|
||||
this.settings.organizingSidebarSavedViews = false
|
||||
}
|
||||
|
||||
deleteSavedView(savedView: PaperlessSavedView) {
|
||||
|
@@ -87,17 +87,28 @@
|
||||
</li>
|
||||
</ul>
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='savedViewService.loading || savedViewService.sidebarViews.length > 0'>
|
||||
<h6 class="sidebar-heading px-3 mt-3 mb-1 text-muted" *ngIf='savedViewService.loading || sidebarViews?.length > 0'>
|
||||
<span i18n>Saved views</span>
|
||||
<div *ngIf="savedViewService.loading" class="spinner-border spinner-border-sm fw-normal ms-2" role="status"></div>
|
||||
</h6>
|
||||
<ul class="nav flex-column mb-2">
|
||||
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
|
||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||
<li class="nav-item w-100" *ngFor="let view of sidebarViews"
|
||||
cdkDrag
|
||||
[cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
|
||||
cdkDragPreviewContainer="parent"
|
||||
cdkDragPreviewClass="navItemDrag"
|
||||
(cdkDragStarted)="onDragStart($event)"
|
||||
(cdkDragEnded)="onDragEnd($event)">
|
||||
<a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||
</svg><span> {{view.name}}</span>
|
||||
</a>
|
||||
<div *ngIf="settingsService.organizingSidebarSavedViews" class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
||||
<svg class="sidebaricon text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grip-vertical"/>
|
||||
</svg>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
@@ -169,6 +169,7 @@ main {
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
list-style-type: none;
|
||||
|
||||
&:hover .close {
|
||||
display: block;
|
||||
@@ -310,3 +311,11 @@ main {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item .position-absolute {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
::ng-deep .navItemDrag .position-absolute svg {
|
||||
display: none;
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { of } from 'rxjs'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
@@ -30,7 +30,47 @@ import { DocumentListViewService } from 'src/app/services/document-list-view.ser
|
||||
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop'
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||
|
||||
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: true,
|
||||
sort_field: 'name',
|
||||
sort_reverse: true,
|
||||
filter_rules: [],
|
||||
},
|
||||
{
|
||||
name: 'Saved View 3',
|
||||
id: 3,
|
||||
show_on_dashboard: true,
|
||||
show_in_sidebar: true,
|
||||
sort_field: 'name',
|
||||
sort_reverse: true,
|
||||
filter_rules: [],
|
||||
},
|
||||
]
|
||||
const document = { id: 2, title: 'Hello world' }
|
||||
|
||||
describe('AppFrameComponent', () => {
|
||||
@@ -60,7 +100,19 @@ describe('AppFrameComponent', () => {
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
SavedViewService,
|
||||
{
|
||||
provide: SavedViewService,
|
||||
useValue: {
|
||||
initialize: () => {},
|
||||
listAll: () =>
|
||||
of({
|
||||
all: [saved_views.map((v) => v.id)],
|
||||
count: saved_views.length,
|
||||
results: saved_views,
|
||||
}),
|
||||
sidebarViews: saved_views.filter((v) => v.show_in_sidebar),
|
||||
},
|
||||
},
|
||||
PermissionsService,
|
||||
RemoteVersionService,
|
||||
IfPermissionsDirective,
|
||||
@@ -269,4 +321,45 @@ describe('AppFrameComponent', () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
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, 'updateSidebarViewsSort')
|
||||
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
||||
jest.spyOn(settingsService, 'storeSettings').mockReturnValue(of(true))
|
||||
component.onDrop({ previousIndex: 0, currentIndex: 1 } as CdkDragDrop<
|
||||
PaperlessSavedView[]
|
||||
>)
|
||||
expect(settingsSpy).toHaveBeenCalledWith([
|
||||
saved_views[2],
|
||||
saved_views[0],
|
||||
saved_views[3],
|
||||
])
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update saved view sorting on drag + drop, show error', () => {
|
||||
jest.spyOn(settingsService, 'get').mockImplementation((key) => {
|
||||
if (key === SETTINGS_KEYS.SIDEBAR_VIEWS_SORT_ORDER) return []
|
||||
})
|
||||
fixture.destroy()
|
||||
fixture = TestBed.createComponent(AppFrameComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
jest
|
||||
.spyOn(settingsService, 'storeSettings')
|
||||
.mockReturnValue(throwError(() => new Error('unable to save')))
|
||||
component.onDrop({ previousIndex: 0, currentIndex: 2 } as CdkDragDrop<
|
||||
PaperlessSavedView[]
|
||||
>)
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
@@ -32,6 +32,13 @@ import {
|
||||
PermissionsService,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||
import {
|
||||
CdkDragStart,
|
||||
CdkDragEnd,
|
||||
CdkDragDrop,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-app-frame',
|
||||
@@ -42,6 +49,17 @@ export class AppFrameComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, ComponentCanDeactivate
|
||||
{
|
||||
versionString = `${environment.appTitle} ${environment.version}`
|
||||
appRemoteVersion: AppRemoteVersion
|
||||
|
||||
isMenuCollapsed: boolean = true
|
||||
|
||||
slimSidebarAnimating: boolean = false
|
||||
|
||||
searchField = new FormControl('')
|
||||
|
||||
sidebarViews: PaperlessSavedView[]
|
||||
|
||||
constructor(
|
||||
public router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -63,7 +81,7 @@ export class AppFrameComponent
|
||||
PermissionType.SavedView
|
||||
)
|
||||
) {
|
||||
savedViewService.initialize()
|
||||
this.savedViewService.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,15 +90,12 @@ export class AppFrameComponent
|
||||
this.checkForUpdates()
|
||||
}
|
||||
this.tasksService.reload()
|
||||
|
||||
this.savedViewService.listAll().subscribe(() => {
|
||||
this.sidebarViews = this.savedViewService.sidebarViews
|
||||
})
|
||||
}
|
||||
|
||||
versionString = `${environment.appTitle} ${environment.version}`
|
||||
appRemoteVersion: AppRemoteVersion
|
||||
|
||||
isMenuCollapsed: boolean = true
|
||||
|
||||
slimSidebarAnimating: boolean = false
|
||||
|
||||
toggleSlimSidebar(): void {
|
||||
this.slimSidebarAnimating = true
|
||||
this.slimSidebarEnabled = !this.slimSidebarEnabled
|
||||
@@ -121,8 +136,6 @@ export class AppFrameComponent
|
||||
return !this.openDocumentsService.hasDirty()
|
||||
}
|
||||
|
||||
searchField = new FormControl('')
|
||||
|
||||
get searchFieldEmpty(): boolean {
|
||||
return this.searchField.value.trim().length == 0
|
||||
}
|
||||
@@ -218,6 +231,27 @@ export class AppFrameComponent
|
||||
})
|
||||
}
|
||||
|
||||
onDragStart(event: CdkDragStart) {
|
||||
this.settingsService.globalDropzoneEnabled = false
|
||||
}
|
||||
|
||||
onDragEnd(event: CdkDragEnd) {
|
||||
this.settingsService.globalDropzoneEnabled = true
|
||||
}
|
||||
|
||||
onDrop(event: CdkDragDrop<PaperlessSavedView[]>) {
|
||||
moveItemInArray(this.sidebarViews, event.previousIndex, event.currentIndex)
|
||||
|
||||
this.settingsService.updateSidebarViewsSort(this.sidebarViews).subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Sidebar views updated`)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error updating sidebar views`, e)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private checkForUpdates() {
|
||||
this.remoteVersionService
|
||||
.checkForUpdates()
|
||||
|
@@ -1,19 +1,3 @@
|
||||
.col-sidebar .row {
|
||||
top: 3.5rem;
|
||||
}
|
||||
|
||||
:host ::ng-deep {
|
||||
.cdk-drag-placeholder {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
/* Animate items as they're being sorted. */
|
||||
.cdk-drop-list-dragging .cdk-drag {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Animate an item that has been dropped. */
|
||||
.cdk-drag-animating {
|
||||
transition: transform 300ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
@@ -29,22 +29,7 @@ export class DashboardComponent extends ComponentWithPermissions {
|
||||
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]
|
||||
this.dashboardViews = this.savedViewService.dashboardViews
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,7 @@
|
||||
<div class="d-flex justify-content-between align-items-center my-2">
|
||||
<div class="progress flex-grow-1">
|
||||
<div *ngFor="let filetype of statistics?.document_file_type_counts; let i = index; let last = last"
|
||||
class="progress-bar bg-primary text-primary-contrast"
|
||||
class="progress-bar bg-primary"
|
||||
role="progressbar"
|
||||
[ngbPopover]="getFileTypeName(filetype)"
|
||||
i18n-ngbPopover
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ngb-alert class="pe-3" type="primary" [dismissible]="true" (closed)="dismiss.emit(true)">
|
||||
<ngb-alert class="pe-3 text-primary-contrast" type="primary" [dismissible]="true" (closed)="dismiss.emit(true)">
|
||||
<h4 class="alert-heading"><ng-container i18n>Paperless-ngx is running!</ng-container> 🎉</h4>
|
||||
<p i18n>You're ready to start uploading documents! Explore the various features of this web app on your own, or start a quick tour using the button below.</p>
|
||||
<p i18n>More detail on how to use and configure Paperless-ngx is always available in the <a href="https://docs.paperless-ngx.com" target="_blank">documentation</a>.</p>
|
||||
|
Reference in New Issue
Block a user