mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge pull request #1868 from paperless-ngx/fix/issue-1866
Fix: independent control of saved views
This commit is contained in:
commit
d6b5c733f3
@ -15,6 +15,7 @@ import { DirtyFormGuard } from './guards/dirty-form.guard'
|
|||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
import { TasksComponent } from './components/manage/tasks/tasks.component'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||||
@ -24,8 +25,16 @@ const routes: Routes = [
|
|||||||
canDeactivate: [DirtyDocGuard],
|
canDeactivate: [DirtyDocGuard],
|
||||||
children: [
|
children: [
|
||||||
{ path: 'dashboard', component: DashboardComponent },
|
{ path: 'dashboard', component: DashboardComponent },
|
||||||
{ path: 'documents', component: DocumentListComponent },
|
{
|
||||||
{ path: 'view/:id', component: DocumentListComponent },
|
path: 'documents',
|
||||||
|
component: DocumentListComponent,
|
||||||
|
canDeactivate: [DirtySavedViewGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'view/:id',
|
||||||
|
component: DocumentListComponent,
|
||||||
|
canDeactivate: [DirtySavedViewGuard],
|
||||||
|
},
|
||||||
{ path: 'documents/:id', component: DocumentDetailComponent },
|
{ path: 'documents/:id', component: DocumentDetailComponent },
|
||||||
{ path: 'asn/:id', component: DocumentAsnComponent },
|
{ path: 'asn/:id', component: DocumentAsnComponent },
|
||||||
{ path: 'tags', component: TagListComponent },
|
{ path: 'tags', component: TagListComponent },
|
||||||
|
@ -69,6 +69,7 @@ import { ColorComponent } from './components/common/input/color/color.component'
|
|||||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||||
import { DocumentCommentsComponent } from './components/document-comments/document-comments.component'
|
import { DocumentCommentsComponent } from './components/document-comments/document-comments.component'
|
||||||
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
import { DirtyDocGuard } from './guards/dirty-doc.guard'
|
||||||
|
import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard'
|
||||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||||
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||||
import { SettingsService } from './services/settings.service'
|
import { SettingsService } from './services/settings.service'
|
||||||
@ -215,6 +216,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
||||||
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
|
||||||
DirtyDocGuard,
|
DirtyDocGuard,
|
||||||
|
DirtySavedViewGuard,
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
@ -16,4 +16,7 @@
|
|||||||
<ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
<ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
||||||
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button *ngIf="alternativeBtnCaption" type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
|
||||||
|
{{alternativeBtnCaption}}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,9 @@ export class ConfirmDialogComponent {
|
|||||||
@Output()
|
@Output()
|
||||||
public confirmClicked = new EventEmitter()
|
public confirmClicked = new EventEmitter()
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public alternativeClicked = new EventEmitter()
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
title = $localize`Confirmation`
|
title = $localize`Confirmation`
|
||||||
|
|
||||||
@ -28,14 +31,22 @@ export class ConfirmDialogComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
btnCaption = $localize`Confirm`
|
btnCaption = $localize`Confirm`
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
alternativeBtnClass = 'btn-secondary'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
alternativeBtnCaption
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
buttonsEnabled = true
|
buttonsEnabled = true
|
||||||
|
|
||||||
confirmButtonEnabled = true
|
confirmButtonEnabled = true
|
||||||
|
alternativeButtonEnabled = true
|
||||||
seconds = 0
|
seconds = 0
|
||||||
secondsTotal = 0
|
secondsTotal = 0
|
||||||
|
|
||||||
confirmSubject: Subject<boolean>
|
confirmSubject: Subject<boolean>
|
||||||
|
alternativeSubject: Subject<boolean>
|
||||||
|
|
||||||
delayConfirm(seconds: number) {
|
delayConfirm(seconds: number) {
|
||||||
const refreshInterval = 0.15 // s
|
const refreshInterval = 0.15 // s
|
||||||
@ -68,4 +79,10 @@ export class ConfirmDialogComponent {
|
|||||||
this.confirmSubject?.next(true)
|
this.confirmSubject?.next(true)
|
||||||
this.confirmSubject?.complete()
|
this.confirmSubject?.complete()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alternative() {
|
||||||
|
this.alternativeClicked.emit()
|
||||||
|
this.alternativeSubject?.next(true)
|
||||||
|
this.alternativeSubject?.complete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,14 +60,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group ms-2 flex-fill" ngbDropdown role="group">
|
<div class="btn-group ms-2 flex-fill" ngbDropdown role="group">
|
||||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle i18n>Views</button>
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
||||||
|
<ng-container i18n>Views</ng-container>
|
||||||
|
<div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle">
|
||||||
|
<span class="visually-hidden">selected</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
|
<div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu>
|
||||||
<ng-container *ngIf="!list.activeSavedViewId">
|
<ng-container *ngIf="!list.activeSavedViewId">
|
||||||
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button>
|
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button>
|
||||||
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" i18n>Save "{{list.activeSavedViewTitle}}"</button>
|
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button>
|
||||||
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
|
<button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,11 @@ import {
|
|||||||
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'
|
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs'
|
||||||
import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule'
|
import {
|
||||||
|
FilterRule,
|
||||||
|
filterRulesDiffer,
|
||||||
|
isFullTextFilterRule,
|
||||||
|
} from 'src/app/data/filter-rule'
|
||||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
||||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||||
@ -54,15 +58,36 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
|||||||
displayMode = 'smallCards' // largeCards, smallCards, details
|
displayMode = 'smallCards' // largeCards, smallCards, details
|
||||||
|
|
||||||
unmodifiedFilterRules: FilterRule[] = []
|
unmodifiedFilterRules: FilterRule[] = []
|
||||||
|
private unmodifiedSavedView: PaperlessSavedView
|
||||||
|
|
||||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
|
|
||||||
|
get savedViewIsModified(): boolean {
|
||||||
|
if (!this.list.activeSavedViewId || !this.unmodifiedSavedView) return false
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
this.unmodifiedSavedView.sort_field !== this.list.sortField ||
|
||||||
|
this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse ||
|
||||||
|
filterRulesDiffer(
|
||||||
|
this.unmodifiedSavedView.filter_rules,
|
||||||
|
this.list.filterRules
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get isFiltered() {
|
get isFiltered() {
|
||||||
return this.list.filterRules?.length > 0
|
return this.list.filterRules?.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle() {
|
getTitle() {
|
||||||
return this.list.activeSavedViewTitle || $localize`Documents`
|
let title = this.list.activeSavedViewTitle
|
||||||
|
if (title && this.savedViewIsModified) {
|
||||||
|
title += '*'
|
||||||
|
} else if (!title) {
|
||||||
|
title = $localize`Documents`
|
||||||
|
}
|
||||||
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
getSortFields() {
|
getSortFields() {
|
||||||
@ -122,7 +147,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
|||||||
this.router.navigate(['404'])
|
this.router.navigate(['404'])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.unmodifiedSavedView = view
|
||||||
this.list.activateSavedViewWithQueryParams(
|
this.list.activateSavedViewWithQueryParams(
|
||||||
view,
|
view,
|
||||||
convertToParamMap(this.route.snapshot.queryParams)
|
convertToParamMap(this.route.snapshot.queryParams)
|
||||||
@ -165,7 +190,8 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
|||||||
this.savedViewService
|
this.savedViewService
|
||||||
.patch(savedView)
|
.patch(savedView)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe((result) => {
|
.subscribe((view) => {
|
||||||
|
this.unmodifiedSavedView = view
|
||||||
this.toastService.showInfo(
|
this.toastService.showInfo(
|
||||||
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
|
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
|
||||||
)
|
)
|
||||||
@ -179,6 +205,7 @@ export class DocumentListComponent implements OnInit, OnDestroy {
|
|||||||
.getCached(viewID)
|
.getCached(viewID)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe((view) => {
|
.subscribe((view) => {
|
||||||
|
this.unmodifiedSavedView = view
|
||||||
this.list.activateSavedView(view)
|
this.list.activateSavedView(view)
|
||||||
this.list.reload()
|
this.list.reload()
|
||||||
})
|
})
|
||||||
|
51
src-ui/src/app/guards/dirty-saved-view.guard.ts
Normal file
51
src-ui/src/app/guards/dirty-saved-view.guard.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { CanDeactivate } from '@angular/router'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { first, Observable, Subject } from 'rxjs'
|
||||||
|
import { DocumentListComponent } from '../components/document-list/document-list.component'
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DirtySavedViewGuard
|
||||||
|
implements CanDeactivate<DocumentListComponent>
|
||||||
|
{
|
||||||
|
constructor(private modalService: NgbModal) {}
|
||||||
|
|
||||||
|
canDeactivate(
|
||||||
|
component: DocumentListComponent
|
||||||
|
): boolean | Observable<boolean> {
|
||||||
|
return component.savedViewIsModified ? this.warn(component) : true
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(component: DocumentListComponent) {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.title = $localize`Unsaved Changes`
|
||||||
|
modal.componentInstance.messageBold =
|
||||||
|
$localize`You have unsaved changes to the saved view` +
|
||||||
|
' "' +
|
||||||
|
component.getTitle()
|
||||||
|
;('".')
|
||||||
|
modal.componentInstance.message = $localize`Are you sure you want to close this saved view?`
|
||||||
|
modal.componentInstance.btnClass = 'btn-secondary'
|
||||||
|
modal.componentInstance.btnCaption = $localize`Close`
|
||||||
|
modal.componentInstance.alternativeBtnClass = 'btn-primary'
|
||||||
|
modal.componentInstance.alternativeBtnCaption = $localize`Save and close`
|
||||||
|
modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => {
|
||||||
|
modal.componentInstance.buttonsEnabled = false
|
||||||
|
component.saveViewConfig()
|
||||||
|
modal.close()
|
||||||
|
})
|
||||||
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
|
modal.componentInstance.buttonsEnabled = false
|
||||||
|
modal.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
const subject = new Subject<boolean>()
|
||||||
|
modal.componentInstance.confirmSubject = subject
|
||||||
|
modal.componentInstance.alternativeSubject = subject
|
||||||
|
|
||||||
|
return subject
|
||||||
|
}
|
||||||
|
}
|
@ -171,15 +171,15 @@ export class DocumentListViewService {
|
|||||||
this.reduceSelectionToFilter()
|
this.reduceSelectionToFilter()
|
||||||
|
|
||||||
if (!this.router.routerState.snapshot.url.includes('/view/')) {
|
if (!this.router.routerState.snapshot.url.includes('/view/')) {
|
||||||
this.router.navigate([], {
|
this.router.navigate(['view', view.id])
|
||||||
queryParams: { view: view.id },
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFromQueryParams(queryParams: ParamMap) {
|
loadFromQueryParams(queryParams: ParamMap) {
|
||||||
const paramsEmpty: boolean = queryParams.keys.length == 0
|
const paramsEmpty: boolean = queryParams.keys.length == 0
|
||||||
let newState: ListViewState = this.listViewStates.get(null)
|
let newState: ListViewState = this.listViewStates.get(
|
||||||
|
this._activeSavedViewId
|
||||||
|
)
|
||||||
if (!paramsEmpty) newState = paramsToViewState(queryParams)
|
if (!paramsEmpty) newState = paramsToViewState(queryParams)
|
||||||
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
|
if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage
|
||||||
|
|
||||||
@ -276,7 +276,6 @@ export class DocumentListViewService {
|
|||||||
) {
|
) {
|
||||||
this.activeListViewState.sortField = 'created'
|
this.activeListViewState.sortField = 'created'
|
||||||
}
|
}
|
||||||
this._activeSavedViewId = null
|
|
||||||
this.activeListViewState.filterRules = filterRules
|
this.activeListViewState.filterRules = filterRules
|
||||||
this.reload()
|
this.reload()
|
||||||
this.reduceSelectionToFilter()
|
this.reduceSelectionToFilter()
|
||||||
@ -288,7 +287,6 @@ export class DocumentListViewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set sortField(field: string) {
|
set sortField(field: string) {
|
||||||
this._activeSavedViewId = null
|
|
||||||
this.activeListViewState.sortField = field
|
this.activeListViewState.sortField = field
|
||||||
this.reload()
|
this.reload()
|
||||||
this.saveDocumentListView()
|
this.saveDocumentListView()
|
||||||
@ -299,7 +297,6 @@ export class DocumentListViewService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set sortReverse(reverse: boolean) {
|
set sortReverse(reverse: boolean) {
|
||||||
this._activeSavedViewId = null
|
|
||||||
this.activeListViewState.sortReverse = reverse
|
this.activeListViewState.sortReverse = reverse
|
||||||
this.reload()
|
this.reload()
|
||||||
this.saveDocumentListView()
|
this.saveDocumentListView()
|
||||||
|
@ -397,6 +397,10 @@ textarea,
|
|||||||
background-color: var(--bs-primary);
|
background-color: var(--bs-primary);
|
||||||
color: var(--pngx-primary-text-contrast);
|
color: var(--pngx-primary-text-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.disabled, &:disabled {
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user