mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-07 07:19:22 +00:00
Merge branch 'dev' into feature/ocr-templates
This commit is contained in:
143 files changed
+8747
-7219
No files matched your search
@@ -1,16 +1,18 @@
|
||||
<div class="row pt-3 pb-2 align-items-center">
|
||||
<div class="col-md text-truncate">
|
||||
<h3 class="d-flex align-items-center mb-2 mb-md-1" style="line-height: 1.4">
|
||||
<span class="text-truncate">{{title()}}</span>
|
||||
@if (id()) {
|
||||
<button type="button" class="badge border-0 bg-primary text-primary-text-contrast ms-3 small fs-normal" (click)="copyID()" title="Copy ID" i18n-title>
|
||||
@if (copied()) {
|
||||
<pngx-bs-icon width="1em" height="1em" name="clipboard-check" class="me-1"></pngx-bs-icon><ng-container i18n>Copied!</ng-container>
|
||||
} @else {
|
||||
ID: {{id()}}
|
||||
}
|
||||
</button>
|
||||
}
|
||||
<span class="d-flex align-items-center mw-100">
|
||||
<span class="text-truncate">{{title()}}</span>
|
||||
@if (id()) {
|
||||
<button type="button" class="badge border-0 bg-primary text-primary-text-contrast ms-3 small fs-normal flex-shrink-0 text-nowrap" (click)="copyID()" title="Copy ID" i18n-title>
|
||||
@if (copied()) {
|
||||
<pngx-bs-icon width="1em" height="1em" name="clipboard-check" class="me-1"></pngx-bs-icon><ng-container i18n>Copied!</ng-container>
|
||||
} @else {
|
||||
ID: {{id()}}
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
@if (subTitle()) {
|
||||
<span class="page-subtitle h6 mb-0 mt-1 fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle()}}</span>
|
||||
}
|
||||
|
||||
+6
-4
@@ -62,13 +62,13 @@
|
||||
} @else {
|
||||
@switch (field) {
|
||||
@case (DisplayField.ADDED) {
|
||||
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a>
|
||||
<a routerLink="/documents/{{doc.id}}" (click)="activateViewForNavigation()" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a>
|
||||
}
|
||||
@case (DisplayField.CREATED) {
|
||||
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created | customDate}}</a>
|
||||
<a routerLink="/documents/{{doc.id}}" (click)="activateViewForNavigation()" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created | customDate}}</a>
|
||||
}
|
||||
@case (DisplayField.TITLE) {
|
||||
<a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
|
||||
<a routerLink="/documents/{{doc.id}}" (click)="activateViewForNavigation()" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
|
||||
}
|
||||
@case (DisplayField.CORRESPONDENT) {
|
||||
@if (doc.correspondent) {
|
||||
@@ -106,7 +106,7 @@
|
||||
}
|
||||
@case (DisplayField.NOTES) {
|
||||
@if (doc.notes.length) {
|
||||
<a routerLink="/documents/{{doc.id}}/notes" class="btn btn-sm p-0">
|
||||
<a routerLink="/documents/{{doc.id}}/notes" (click)="activateViewForNavigation()" class="btn btn-sm p-0">
|
||||
<span class="badge rounded-pill bg-light border text-primary">
|
||||
<pngx-bs-icon width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></pngx-bs-icon>
|
||||
{{doc.notes.length}}</span>
|
||||
@@ -150,6 +150,7 @@
|
||||
<pngx-document-card-small
|
||||
class="p-0"
|
||||
[ngStyle]="{ opacity: show() ? 1 : 1 - (i * 1/documents().length) }"
|
||||
(click)="activateViewForNavigation()"
|
||||
(dblClickDocument)="openDocumentDetail(d)"
|
||||
[document]="d"
|
||||
[displayFields]="displayFields()"
|
||||
@@ -173,6 +174,7 @@
|
||||
}
|
||||
@for (d of documents(); track d.id; let i = $index) {
|
||||
<pngx-document-card-large
|
||||
(click)="activateViewForNavigation()"
|
||||
(dblClickDocument)="openDocumentDetail(d)"
|
||||
[document]="d"
|
||||
[ngStyle]="{ opacity: show() ? 1 : 1 - (i * 1/documents().length) }"
|
||||
|
||||
+35
-1
@@ -16,7 +16,11 @@ import { routes } from 'src/app/app-routing.module'
|
||||
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
|
||||
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
||||
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { DisplayField, DisplayMode } from 'src/app/data/document'
|
||||
import {
|
||||
DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
|
||||
DisplayField,
|
||||
DisplayMode,
|
||||
} from 'src/app/data/document'
|
||||
import {
|
||||
FILTER_CORRESPONDENT,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
@@ -277,10 +281,40 @@ describe('SavedViewWidgetComponent', () => {
|
||||
|
||||
it('should navigate to document', () => {
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
const activateSpy = jest.spyOn(
|
||||
documentListViewService,
|
||||
'activateSavedViewWithDocuments'
|
||||
)
|
||||
component.openDocumentDetail(documentResults[0])
|
||||
expect(activateSpy).toHaveBeenCalled()
|
||||
expect(routerSpy).toHaveBeenCalledWith(['documents', documentResults[0].id])
|
||||
})
|
||||
|
||||
it('should hand its documents to the list service when opening a document', () => {
|
||||
jest.spyOn(documentService, 'listFiltered').mockReturnValue(
|
||||
of({
|
||||
all: [2, 3],
|
||||
count: 25,
|
||||
results: documentResults,
|
||||
})
|
||||
)
|
||||
jest.spyOn(router, 'navigateByUrl').mockResolvedValue(true)
|
||||
const activateSpy = jest
|
||||
.spyOn(documentListViewService, 'activateSavedViewWithDocuments')
|
||||
.mockImplementation(() => {})
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
fixture.debugElement
|
||||
.query(By.css('a[title="Open document"]'))
|
||||
.nativeElement.click()
|
||||
expect(activateSpy).toHaveBeenCalledWith(savedView, documentResults, 25, 20)
|
||||
})
|
||||
|
||||
it('should fall back to the default dashboard page size', () => {
|
||||
component.savedView = { ...savedView, page_size: undefined }
|
||||
expect(component.pageSize).toEqual(DEFAULT_DASHBOARD_VIEW_PAGE_SIZE)
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click tag', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickTag(11, new MouseEvent('click'))
|
||||
|
||||
+19
-2
@@ -132,7 +132,7 @@ export class SavedViewWidgetComponent
|
||||
ngOnInit(): void {
|
||||
this.placeholderRows = Array.from(
|
||||
{
|
||||
length: this.savedView?.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
|
||||
length: this.pageSize,
|
||||
},
|
||||
(_, index) => index
|
||||
)
|
||||
@@ -187,7 +187,7 @@ export class SavedViewWidgetComponent
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
1,
|
||||
this.savedView?.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
|
||||
this.pageSize,
|
||||
this.savedView.sort_field,
|
||||
this.savedView.sort_reverse,
|
||||
this.savedView.filter_rules,
|
||||
@@ -292,7 +292,24 @@ export class SavedViewWidgetComponent
|
||||
])
|
||||
}
|
||||
|
||||
get pageSize(): number {
|
||||
return this.savedView?.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass the widget's documents to the list service to enable next / previous.
|
||||
*/
|
||||
activateViewForNavigation() {
|
||||
this.documentListViewService.activateSavedViewWithDocuments(
|
||||
this.savedView,
|
||||
this.documents(),
|
||||
this.count(),
|
||||
this.pageSize
|
||||
)
|
||||
}
|
||||
|
||||
openDocumentDetail(document: Document) {
|
||||
this.activateViewForNavigation()
|
||||
this.router.navigate(['documents', document.id])
|
||||
}
|
||||
|
||||
|
||||
@@ -452,21 +452,22 @@
|
||||
</div>
|
||||
|
||||
<ng-template #saveButtons>
|
||||
@let saveDisabled = !userCanEdit || networkActive() || (isDirty$ | async) !== true;
|
||||
<div class="btn-group">
|
||||
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||
<button type="submit" class="order-3 btn btn-sm btn-primary" title="Save" i18n-title [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">
|
||||
<button type="submit" class="order-3 btn btn-sm" [class.btn-primary]="!saveDisabled" [class.btn-outline-primary]="saveDisabled" title="Save" i18n-title [disabled]="saveDisabled">
|
||||
<pngx-bs-icon class="d-md-none" width="1.2em" height="1.2em" name="floppy"></pngx-bs-icon><span class="d-none d-md-inline" i18n>Save</span>
|
||||
</button>
|
||||
@if (hasNext()) {
|
||||
<button type="button" class="order-1 btn btn-sm btn-outline-primary d-none d-md-inline-block" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">Save & next</button>
|
||||
<button type="button" class="order-1 btn btn-sm btn-outline-primary d-none d-md-inline-block" (click)="saveEditNext()" i18n [disabled]="saveDisabled">Save & next</button>
|
||||
} @else {
|
||||
<button type="button" class="order-2 btn btn-sm btn-outline-primary d-none d-md-inline-block" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">Save & close</button>
|
||||
<button type="button" class="order-2 btn btn-sm btn-outline-primary d-none d-md-inline-block" (click)="save(true)" i18n [disabled]="saveDisabled">Save & close</button>
|
||||
}
|
||||
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" title="Discard" i18n-title [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">
|
||||
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" title="Discard" i18n-title [disabled]="saveDisabled">
|
||||
<pngx-bs-icon class="d-md-none" width="1.2em" height="1.2em" name="arrow-counterclockwise"></pngx-bs-icon><span class="d-none d-md-inline" i18n>Discard</span>
|
||||
</button>
|
||||
<div class="btn-group order-4 d-md-none" ngbDropdown placement="bottom-end top-end" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle dropdown-toggle-split" ngbDropdownToggle aria-label="More save options" i18n-aria-label [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true"></button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle dropdown-toggle-split" ngbDropdownToggle aria-label="More save options" i18n-aria-label [disabled]="saveDisabled"></button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
@if (hasNext()) {
|
||||
<button type="button" ngbDropdownItem (click)="saveEditNext()" i18n>Save & next</button>
|
||||
|
||||
@@ -54,7 +54,16 @@ textarea.rtl {
|
||||
border-color: var(--bs-border-color);
|
||||
}
|
||||
|
||||
// save buttons are visually reordered with order-*, fix btn-group border overlap here
|
||||
.btn-group > .order-1,
|
||||
.btn-group > .order-2,
|
||||
.btn-group > .order-3,
|
||||
.btn-group > .order-4 {
|
||||
margin-left: calc(-1 * var(--bs-border-width));
|
||||
}
|
||||
|
||||
.btn-group .btn.order-0 {
|
||||
margin-left: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-left-radius: var(--bs-border-radius-sm);
|
||||
|
||||
@@ -940,6 +940,20 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['view', 77])
|
||||
})
|
||||
|
||||
it('should close back to the dashboard even if a saved view is active', () => {
|
||||
initNormally()
|
||||
jest
|
||||
.spyOn(componentRouterService, 'getComponentURLBefore')
|
||||
.mockReturnValue('dashboard')
|
||||
jest
|
||||
.spyOn(documentListViewService, 'activeSavedViewId', 'get')
|
||||
.mockReturnValue(77)
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.close()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['dashboard'])
|
||||
expect(navigateSpy).not.toHaveBeenCalledWith(['view', 77])
|
||||
})
|
||||
|
||||
it('should not close if e.g. user-cancelled', () => {
|
||||
initNormally()
|
||||
jest.spyOn(openDocumentsService, 'closeDocument').mockReturnValue(of(false))
|
||||
|
||||
@@ -1463,7 +1463,12 @@ export class DocumentDetailComponent
|
||||
.subscribe((closed) => {
|
||||
if (!closed) return
|
||||
if (closedCallback) closedCallback()
|
||||
if (this.documentListViewService.activeSavedViewId) {
|
||||
if (
|
||||
this.componentRouterService.getComponentURLBefore() === 'dashboard'
|
||||
) {
|
||||
// e.g. opened from a dashboard widget
|
||||
this.router.navigate(['dashboard'])
|
||||
} else if (this.documentListViewService.activeSavedViewId) {
|
||||
this.router.navigate([
|
||||
'view',
|
||||
this.documentListViewService.activeSavedViewId,
|
||||
|
||||
@@ -411,6 +411,7 @@ export class DocumentListComponent
|
||||
display_mode: this.list.displayMode,
|
||||
display_fields: this.activeDisplayFields,
|
||||
}
|
||||
const title = this.list.activeSavedViewTitle
|
||||
this.savedViewService
|
||||
.patch(savedView)
|
||||
.pipe(first())
|
||||
@@ -419,13 +420,13 @@ export class DocumentListComponent
|
||||
this.activeSavedView = view
|
||||
this.unmodifiedSavedView = view
|
||||
this.toastService.showSuccess(
|
||||
$localize`View "${this.list.activeSavedViewTitle}" saved successfully.`
|
||||
$localize`View "${view.name ?? title}" saved successfully.`
|
||||
)
|
||||
this.unmodifiedFilterRules.set(this.list.filterRules)
|
||||
},
|
||||
error: (err) => {
|
||||
this.toastService.showError(
|
||||
$localize`Failed to save view "${this.list.activeSavedViewTitle}".`,
|
||||
$localize`Failed to save view "${title}".`,
|
||||
err
|
||||
)
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user