mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-20 10:54:58 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80210bd3bf | ||
|
|
d34ef75786 | ||
|
|
b8c424c8e1 | ||
|
|
ec8991c2ec | ||
|
|
4f6d9fa93f | ||
|
|
2180b21c41 | ||
|
|
99bdfdfe7a | ||
|
|
73a0586a75 | ||
|
|
dcba44ad78 | ||
|
|
ed54f1a8b9 |
+140
-136
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -14,7 +14,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div content class="wrapper fade" [class.show]="show()">
|
<div content class="wrapper fade" [class.show]="show()">
|
||||||
@if (displayMode() === DisplayMode.TABLE) {
|
@if (error()) {
|
||||||
|
<div class="alert alert-danger mb-0" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{error()}}</div>
|
||||||
|
} @else if (displayMode() === DisplayMode.TABLE) {
|
||||||
<table class="table table-hover mb-0 mt-n2 align-middle">
|
<table class="table table-hover mb-0 mt-n2 align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
+27
-2
@@ -1,6 +1,10 @@
|
|||||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||||
import { DatePipe } from '@angular/common'
|
import { DatePipe } from '@angular/common'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import {
|
||||||
|
HttpErrorResponse,
|
||||||
|
provideHttpClient,
|
||||||
|
withInterceptorsFromDi,
|
||||||
|
} from '@angular/common/http'
|
||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
@@ -8,7 +12,7 @@ import { Router } from '@angular/router'
|
|||||||
import { RouterTestingModule } from '@angular/router/testing'
|
import { RouterTestingModule } from '@angular/router/testing'
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { Subject, of } from 'rxjs'
|
import { Subject, of, throwError } from 'rxjs'
|
||||||
import { routes } from 'src/app/app-routing.module'
|
import { routes } from 'src/app/app-routing.module'
|
||||||
import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component'
|
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 { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
|
||||||
@@ -230,6 +234,27 @@ describe('SavedViewWidgetComponent', () => {
|
|||||||
expect(component.documents()).toEqual(documentResults)
|
expect(component.documents()).toEqual(documentResults)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show an error if documents fail to load', () => {
|
||||||
|
jest.spyOn(documentService, 'listFiltered').mockReturnValue(
|
||||||
|
throwError(
|
||||||
|
() =>
|
||||||
|
new HttpErrorResponse({
|
||||||
|
error: { added__date__lte: ['Enter a valid date.'] },
|
||||||
|
status: 400,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
component.reload()
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(component.loading()).toBe(false)
|
||||||
|
expect(component.error()).toEqual('Added: Enter a valid date.')
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||||
|
'Error while loading documents: Added: Enter a valid date.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should reload on document consumption finished', () => {
|
it('should reload on document consumption finished', () => {
|
||||||
const fileStatusSubject = new Subject<FileStatus>()
|
const fileStatusSubject = new Subject<FileStatus>()
|
||||||
jest
|
jest
|
||||||
|
|||||||
+37
-6
@@ -125,6 +125,8 @@ export class SavedViewWidgetComponent
|
|||||||
|
|
||||||
readonly count = signal<number>(null)
|
readonly count = signal<number>(null)
|
||||||
|
|
||||||
|
readonly error = signal<string | null>(null)
|
||||||
|
|
||||||
placeholderRows: number[] = []
|
placeholderRows: number[] = []
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -180,6 +182,7 @@ export class SavedViewWidgetComponent
|
|||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
this.loading.set(this.documents().length == 0)
|
this.loading.set(this.documents().length == 0)
|
||||||
|
this.error.set(null)
|
||||||
this.show.set(true)
|
this.show.set(true)
|
||||||
this.documentService
|
this.documentService
|
||||||
.listFiltered(
|
.listFiltered(
|
||||||
@@ -191,12 +194,40 @@ export class SavedViewWidgetComponent
|
|||||||
{ truncate_content: true }
|
{ truncate_content: true }
|
||||||
)
|
)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe((result) => {
|
.subscribe({
|
||||||
this.documents.set(result.results)
|
next: (result) => {
|
||||||
this.count.set(result.count)
|
this.documents.set(result.results)
|
||||||
this.savedViewService.setDocumentCount(this.savedView, result.count)
|
this.count.set(result.count)
|
||||||
this.loading.set(false)
|
this.savedViewService.setDocumentCount(this.savedView, result.count)
|
||||||
this.show.set(true)
|
this.loading.set(false)
|
||||||
|
this.show.set(true)
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.documents.set([])
|
||||||
|
this.count.set(null)
|
||||||
|
let errorMessage
|
||||||
|
if (
|
||||||
|
typeof error.error === 'object' &&
|
||||||
|
Object.keys(error.error).length > 0
|
||||||
|
) {
|
||||||
|
errorMessage = Object.keys(error.error)
|
||||||
|
.map((fieldName) => {
|
||||||
|
const fieldNameBase = fieldName.split('__')[0]
|
||||||
|
const fieldError: Array<string> = error.error[fieldName]
|
||||||
|
return `${
|
||||||
|
this.documentService.sortFields.find(
|
||||||
|
(f) => f.field?.split('__')[0] == fieldNameBase
|
||||||
|
)?.name ?? fieldNameBase
|
||||||
|
}: ${fieldError[0]}`
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
} else {
|
||||||
|
errorMessage = error.error
|
||||||
|
}
|
||||||
|
this.error.set(errorMessage)
|
||||||
|
this.loading.set(false)
|
||||||
|
this.show.set(true)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -482,9 +482,7 @@
|
|||||||
</pngx-pdf-viewer>
|
</pngx-pdf-viewer>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%">
|
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%"></object>
|
||||||
<span>Preview is unavailable.</span>
|
|
||||||
</object>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@case (ContentRenderType.Text) {
|
@case (ContentRenderType.Text) {
|
||||||
@@ -505,9 +503,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@case (ContentRenderType.Other) {
|
@case (ContentRenderType.Other) {
|
||||||
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%">
|
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%"></object>
|
||||||
<span>Preview is unavailable.</span>
|
|
||||||
</object>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (requiresPassword) {
|
@if (requiresPassword) {
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-2 doc-img-container rounded-start" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()">
|
<div class="col-md-2 doc-img-container rounded-start" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()">
|
||||||
@if (document()) {
|
@if (document()) {
|
||||||
<img [src]="getThumbUrl()" class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
|
<img [ngSrc]="getThumbUrl()" fill class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
|
||||||
|
|
||||||
<div class="border-end border-bottom bg-light document-card-check">
|
<div class="border-end border-bottom bg-light document-card-check">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
|||||||
+6
@@ -88,6 +88,12 @@ describe('DocumentCardLargeComponent', () => {
|
|||||||
expect(fixture.nativeElement.textContent).toContain('8 pages')
|
expect(fixture.nativeElement.textContent).toContain('8 pages')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should lazy load the thumbnail', () => {
|
||||||
|
const thumbnail: HTMLImageElement =
|
||||||
|
fixture.nativeElement.querySelector('img.doc-img')
|
||||||
|
expect(thumbnail.getAttribute('loading')).toEqual('lazy')
|
||||||
|
})
|
||||||
|
|
||||||
it('should trim content', () => {
|
it('should trim content', () => {
|
||||||
expect(component.contentTrimmed).toHaveLength(503) // includes ...
|
expect(component.contentTrimmed).toHaveLength(503) // includes ...
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
import { AsyncPipe, NgOptimizedImage } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
@@ -46,6 +46,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
TagComponent,
|
TagComponent,
|
||||||
CustomFieldDisplayComponent,
|
CustomFieldDisplayComponent,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
NgOptimizedImage,
|
||||||
UsernamePipe,
|
UsernamePipe,
|
||||||
CorrespondentNamePipe,
|
CorrespondentNamePipe,
|
||||||
DocumentTypeNamePipe,
|
DocumentTypeNamePipe,
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<div class="card h-100 shadow-sm document-card" [class.placeholder-glow]="!document()" [class.card-selected]="selected()" (mouseleave)="mouseLeaveCard()">
|
<div class="card h-100 shadow-sm document-card" [class.placeholder-glow]="!document()" [class.card-selected]="selected()" (mouseleave)="mouseLeaveCard()">
|
||||||
<div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
|
<div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
|
||||||
@if (document()) {
|
@if (document()) {
|
||||||
<img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
|
<img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [ngSrc]="getThumbUrl()" fill>
|
||||||
|
|
||||||
<div class="border-end border-bottom bg-light py-1 px-2 document-card-check">
|
<div class="border-end border-bottom bg-light py-1 px-2 document-card-check">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
|||||||
+5
-1
@@ -22,10 +22,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.doc-img-container {
|
||||||
|
position: relative;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
.doc-img {
|
.doc-img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: top left;
|
object-position: top left;
|
||||||
height: 180px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.document-card-check {
|
.document-card-check {
|
||||||
|
|||||||
+6
@@ -61,6 +61,12 @@ describe('DocumentCardSmallComponent', () => {
|
|||||||
expect(fixture.nativeElement.textContent).toContain('12 pages')
|
expect(fixture.nativeElement.textContent).toContain('12 pages')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should lazy load the thumbnail', () => {
|
||||||
|
const thumbnail: HTMLImageElement =
|
||||||
|
fixture.nativeElement.querySelector('img.doc-img')
|
||||||
|
expect(thumbnail.getAttribute('loading')).toEqual('lazy')
|
||||||
|
})
|
||||||
|
|
||||||
it('should display a document, limit tags to 5', () => {
|
it('should display a document, limit tags to 5', () => {
|
||||||
expect(fixture.nativeElement.textContent).toContain('Document 10')
|
expect(fixture.nativeElement.textContent).toContain('Document 10')
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { AsyncPipe } from '@angular/common'
|
import { AsyncPipe, NgOptimizedImage } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
Component,
|
Component,
|
||||||
@@ -46,6 +46,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
TagComponent,
|
TagComponent,
|
||||||
CustomFieldDisplayComponent,
|
CustomFieldDisplayComponent,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
|
NgOptimizedImage,
|
||||||
UsernamePipe,
|
UsernamePipe,
|
||||||
CorrespondentNamePipe,
|
CorrespondentNamePipe,
|
||||||
DocumentTypeNamePipe,
|
DocumentTypeNamePipe,
|
||||||
|
|||||||
@@ -177,6 +177,49 @@ class TestViews(DirectoriesMixin, TestCase):
|
|||||||
self.assertEqual(response.request["PATH_INFO"], "/accounts/login/")
|
self.assertEqual(response.request["PATH_INFO"], "/accounts/login/")
|
||||||
self.assertContains(response, b"Share link has expired")
|
self.assertContains(response, b"Share link has expired")
|
||||||
|
|
||||||
|
def test_share_link_archive_falls_back_to_original(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A document without an archive version
|
||||||
|
- A share link using the default archive file version
|
||||||
|
WHEN:
|
||||||
|
- An unauthenticated request for the share link is made
|
||||||
|
THEN:
|
||||||
|
- The original document is returned
|
||||||
|
"""
|
||||||
|
_, filename = tempfile.mkstemp(dir=self.dirs.originals_dir)
|
||||||
|
content = b"This document has no archive"
|
||||||
|
|
||||||
|
with Path(filename).open("wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="no archive",
|
||||||
|
filename=Path(filename).name,
|
||||||
|
mime_type="text/plain",
|
||||||
|
)
|
||||||
|
|
||||||
|
sharelink_permissions = Permission.objects.filter(
|
||||||
|
codename__contains="sharelink",
|
||||||
|
)
|
||||||
|
self.user.user_permissions.add(*sharelink_permissions)
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
create_response = self.client.post(
|
||||||
|
"/api/share_links/",
|
||||||
|
{"document": doc.pk},
|
||||||
|
)
|
||||||
|
self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)
|
||||||
|
share_link = ShareLink.objects.get(document=doc)
|
||||||
|
self.assertEqual(share_link.file_version, ShareLink.FileVersion.ARCHIVE)
|
||||||
|
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
response = self.client.get(f"/share/{share_link.slug}")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(read_streaming_response(response), content)
|
||||||
|
|
||||||
def test_list_with_full_permissions(self) -> None:
|
def test_list_with_full_permissions(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
|||||||
@@ -4534,7 +4534,8 @@ class SharedLinkView(View):
|
|||||||
return HttpResponseRedirect("/accounts/login/?sharelink_expired=1")
|
return HttpResponseRedirect("/accounts/login/?sharelink_expired=1")
|
||||||
return serve_file(
|
return serve_file(
|
||||||
doc=share_link.document,
|
doc=share_link.document,
|
||||||
use_archive=share_link.file_version == "archive",
|
use_archive=share_link.file_version == ShareLink.FileVersion.ARCHIVE
|
||||||
|
and share_link.document.has_archive_version,
|
||||||
disposition="inline",
|
disposition="inline",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
|
"POT-Creation-Date: 2026-07-19 20:55+0000\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@@ -53,7 +53,7 @@ msgstr ""
|
|||||||
msgid "Maximum nesting depth exceeded."
|
msgid "Maximum nesting depth exceeded."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/filters.py:1052
|
#: documents/filters.py:1059
|
||||||
msgid "Custom field not found"
|
msgid "Custom field not found"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1686,11 +1686,11 @@ msgstr ""
|
|||||||
msgid "Bundle is already being processed."
|
msgid "Bundle is already being processed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:4554
|
#: documents/views.py:4555
|
||||||
msgid "The share link bundle is still being prepared. Please try again later."
|
msgid "The share link bundle is still being prepared. Please try again later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:4564
|
#: documents/views.py:4565
|
||||||
msgid "The share link bundle is unavailable."
|
msgid "The share link bundle is unavailable."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user