Compare commits

...
4 Commits
8 changed files with 179 additions and 130 deletions
+46
View File
@@ -1,5 +1,51 @@
# Changelog # Changelog
## paperless-ngx 3.0.4
### Bug Fixes
- Fix: prevent pdfjs highlight scrolling from affecting the entire page [@shamoon](https://github.com/shamoon) ([#13355](https://github.com/paperless-ngx/paperless-ngx/pull/13355))
- Fix: don't skip OCR/archive for tagged PDFs with no actual text [@stumpylog](https://github.com/stumpylog) ([#13351](https://github.com/paperless-ngx/paperless-ngx/pull/13351))
- Performance: more efficient diacritic normalization in dropdown filtering [@shamoon](https://github.com/shamoon) ([#13347](https://github.com/paperless-ngx/paperless-ngx/pull/13347))
- Fix: dedupe permission-visible documents when combined with multi-tag filters [@stumpylog](https://github.com/stumpylog) ([#13345](https://github.com/paperless-ngx/paperless-ngx/pull/13345))
- Fixhancement: pass LLM output language to chat if specified [@shamoon](https://github.com/shamoon) ([#13340](https://github.com/paperless-ngx/paperless-ngx/pull/13340))
- Fix: ensure preview reload on live changes [@shamoon](https://github.com/shamoon) ([#13321](https://github.com/paperless-ngx/paperless-ngx/pull/13321))
- Fix: clamp all out out-of-range possible fields [@stumpylog](https://github.com/stumpylog) ([#13316](https://github.com/paperless-ngx/paperless-ngx/pull/13316))
- Fix: add docstring to DocumentClassifierSchema for cleaner LLM tool description [@stumpylog](https://github.com/stumpylog) ([#13315](https://github.com/paperless-ngx/paperless-ngx/pull/13315))
- Fix: guard build\_document\_node against stale FK on deleted correspondent/doc type [@stumpylog](https://github.com/stumpylog) ([#13318](https://github.com/paperless-ngx/paperless-ngx/pull/13318))
- Fix: prevent search filter loss when closing document with Escape key [@shamoon](https://github.com/shamoon) ([#13317](https://github.com/paperless-ngx/paperless-ngx/pull/13317))
- Fix: fix frontend permissions display for stats/system perms [@shamoon](https://github.com/shamoon) ([#13305](https://github.com/paperless-ngx/paperless-ngx/pull/13305))
### Documentation
- Documentation: fix PAPERLESS\_AI\_LLM\_OUTPUT\_LANGUAGE heading level [@SuperSandro2000](https://github.com/SuperSandro2000) ([#13341](https://github.com/paperless-ngx/paperless-ngx/pull/13341))
### Dependencies
- Chore: resolve npm provenance issues with chokidar and semver [@shamoon](https://github.com/shamoon) ([#13323](https://github.com/paperless-ngx/paperless-ngx/pull/13323))
### All App Changes
<details>
<summary>14 changes</summary>
- Fix: prevent pdfjs highlight scrolling from affecting the entire page [@shamoon](https://github.com/shamoon) ([#13355](https://github.com/paperless-ngx/paperless-ngx/pull/13355))
- Fix: don't skip OCR/archive for tagged PDFs with no actual text [@stumpylog](https://github.com/stumpylog) ([#13351](https://github.com/paperless-ngx/paperless-ngx/pull/13351))
- Performance: more efficient diacritic normalization in dropdown filtering [@shamoon](https://github.com/shamoon) ([#13347](https://github.com/paperless-ngx/paperless-ngx/pull/13347))
- Performance: prefetch notes and custom fields for LLM index text building [@stumpylog](https://github.com/stumpylog) ([#13350](https://github.com/paperless-ngx/paperless-ngx/pull/13350))
- Fix: dedupe permission-visible documents when combined with multi-tag filters [@stumpylog](https://github.com/stumpylog) ([#13345](https://github.com/paperless-ngx/paperless-ngx/pull/13345))
- Performance: Scope llm index updates to actually modified documents [@stumpylog](https://github.com/stumpylog) ([#13322](https://github.com/paperless-ngx/paperless-ngx/pull/13322))
- Fixhancement: pass LLM output language to chat if specified [@shamoon](https://github.com/shamoon) ([#13340](https://github.com/paperless-ngx/paperless-ngx/pull/13340))
- Chore: resolve npm provenance issues with chokidar and semver [@shamoon](https://github.com/shamoon) ([#13323](https://github.com/paperless-ngx/paperless-ngx/pull/13323))
- Fix: ensure preview reload on live changes [@shamoon](https://github.com/shamoon) ([#13321](https://github.com/paperless-ngx/paperless-ngx/pull/13321))
- Fix: clamp all out out-of-range possible fields [@stumpylog](https://github.com/stumpylog) ([#13316](https://github.com/paperless-ngx/paperless-ngx/pull/13316))
- Fix: add docstring to DocumentClassifierSchema for cleaner LLM tool description [@stumpylog](https://github.com/stumpylog) ([#13315](https://github.com/paperless-ngx/paperless-ngx/pull/13315))
- Fix: guard build\_document\_node against stale FK on deleted correspondent/doc type [@stumpylog](https://github.com/stumpylog) ([#13318](https://github.com/paperless-ngx/paperless-ngx/pull/13318))
- Fix: prevent search filter loss when closing document with Escape key [@shamoon](https://github.com/shamoon) ([#13317](https://github.com/paperless-ngx/paperless-ngx/pull/13317))
- Fix: fix frontend permissions display for stats/system perms [@shamoon](https://github.com/shamoon) ([#13305](https://github.com/paperless-ngx/paperless-ngx/pull/13305))
</details>
## paperless-ngx 3.0.3 ## paperless-ngx 3.0.3
### Bug Fixes ### Bug Fixes
@@ -21,7 +21,7 @@
</div> </div>
</pngx-page-header> </pngx-page-header>
@if (!tasksService.completedFileTasks && tasksService.loading) { @if (loading() && pagedTasks().length === 0) {
<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>
} }
@@ -2269,4 +2269,34 @@ describe('FilterEditorComponent', () => {
component.itemSelected({ item: 'world', preventDefault: () => true }) component.itemSelected({ item: 'world', preventDefault: () => true })
expect(component.textFilter).toEqual('hello world ') expect(component.textFilter).toEqual('hello world ')
}) })
it('should choose the active autocomplete item with Enter', () => {
component.textFilterTarget = 'fulltext-query'
jest
.spyOn(searchService, 'autocomplete')
.mockReturnValue(of(['hello', 'help']))
const input = component.textFilterInput.nativeElement as HTMLInputElement
input.value = 'he'
input.dispatchEvent(new Event('input'))
tick(250)
input.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowDown',
bubbles: true,
cancelable: true,
})
)
input.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Enter',
bubbles: true,
cancelable: true,
})
)
fixture.detectChanges()
expect(component.textFilter).toEqual('help ')
})
}) })
@@ -1314,6 +1314,10 @@ export class FilterEditorComponent
textFilterKeydown(event: KeyboardEvent) { textFilterKeydown(event: KeyboardEvent) {
if (event.key == 'Enter') { if (event.key == 'Enter') {
if (event.defaultPrevented) {
// NgbTypeahead calls preventDefault, so use that to detect if the Enter key was for the dropdown
return
}
const filterString = ( const filterString = (
this.textFilterInput.nativeElement as HTMLInputElement this.textFilterInput.nativeElement as HTMLInputElement
).value ).value
+56 -76
View File
@@ -50,13 +50,66 @@ describe('TasksService', () => {
req.flush({ count: 0, results: [] }) req.flush({ count: 0, results: [] })
}) })
it('does not call tasks api endpoint on reload if already loading', () => { it('cancels an in-progress reload when reloading again', () => {
tasksService.loading = true
tasksService.reload() tasksService.reload()
httpTestingController.expectNone( const staleReload = httpTestingController.expectOne(
(req: HttpRequest<unknown>) => (req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` req.url === `${environment.apiBaseUrl}tasks/`
) )
tasksService.reload()
expect(staleReload.cancelled).toBe(true)
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
)
.flush({ count: 0, results: [] })
})
it('continues reloading after a reload request fails', () => {
tasksService.reload()
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
)
.flush('error', { status: 500, statusText: 'error' })
expect(tasksService.loading).toBe(false)
tasksService.reload()
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
)
.flush({ count: 0, results: [] })
})
it('reloads after dismissing a task while a reload is already in progress', () => {
tasksService.reload()
const staleReload = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false'
)
tasksService.dismissTasks(new Set([1])).subscribe()
httpTestingController
.expectOne(`${environment.apiBaseUrl}tasks/acknowledge/`)
.flush([])
expect(staleReload.cancelled).toBe(true)
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false'
)
.flush({ count: 0, results: [] })
expect(tasksService.needsAttentionTasks).toHaveLength(0)
}) })
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => { it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
@@ -101,79 +154,6 @@ describe('TasksService', () => {
.flush({ count: 0, results: [] }) .flush({ count: 0, results: [] })
}) })
it('groups mixed task types by status when reloading', () => {
expect(tasksService.total).toEqual(0)
const mockTasks = [
{
task_type: PaperlessTaskType.ConsumeFile,
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
status: PaperlessTaskStatus.Success,
acknowledged: false,
task_id: '1234',
input_data: { filename: 'file1.pdf' },
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.SanityCheck,
trigger_source: PaperlessTaskTriggerSource.System,
status: PaperlessTaskStatus.Failure,
acknowledged: false,
task_id: '1235',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.MailFetch,
trigger_source: PaperlessTaskTriggerSource.Scheduled,
status: PaperlessTaskStatus.Pending,
acknowledged: false,
task_id: '1236',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.LlmIndex,
trigger_source: PaperlessTaskTriggerSource.WebUI,
status: PaperlessTaskStatus.Started,
acknowledged: false,
task_id: '1237',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.EmptyTrash,
trigger_source: PaperlessTaskTriggerSource.Manual,
status: PaperlessTaskStatus.Success,
acknowledged: false,
task_id: '1238',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
]
tasksService.reload()
const req = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
req.flush({ count: mockTasks.length, results: mockTasks })
expect(tasksService.allFileTasks).toHaveLength(5)
expect(tasksService.completedFileTasks).toHaveLength(2)
expect(tasksService.failedFileTasks).toHaveLength(1)
expect(tasksService.queuedFileTasks).toHaveLength(1)
expect(tasksService.startedFileTasks).toHaveLength(1)
})
it('includes revoked tasks in needs attention', () => { it('includes revoked tasks in needs attention', () => {
const mockTasks = [ const mockTasks = [
{ {
+39 -50
View File
@@ -1,7 +1,15 @@
import { HttpClient } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { Injectable, inject, signal } from '@angular/core' import { Injectable, inject, signal } from '@angular/core'
import { Observable, Subject } from 'rxjs' import { EMPTY, Observable, Subject } from 'rxjs'
import { first, map, takeUntil, tap } from 'rxjs/operators' import {
catchError,
finalize,
first,
map,
switchMap,
takeUntil,
tap,
} from 'rxjs/operators'
import { import {
PaperlessTask, PaperlessTask,
PaperlessTaskStatus, PaperlessTaskStatus,
@@ -23,44 +31,40 @@ export class TasksService {
public loading: boolean = false public loading: boolean = false
private readonly fileTasks = signal<PaperlessTask[]>([]) private readonly tasks = signal<PaperlessTask[]>([])
private readonly reloadNotifier = new Subject<void>()
private unsubscribeNotifer: Subject<any> = new Subject() private unsubscribeNotifer: Subject<any> = new Subject()
public get total(): number { constructor() {
return this.fileTasks().length this.reloadNotifier
} .pipe(
switchMap(() => {
public get allFileTasks(): PaperlessTask[] { this.loading = true
return this.fileTasks().slice(0) return this.http
} .get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
params: {
public get queuedFileTasks(): PaperlessTask[] { acknowledged: 'false',
return this.fileTasks().filter( page_size: this.defaultReloadPageSize,
(t) => t.status === PaperlessTaskStatus.Pending },
) })
} .pipe(
map((response) => response.results),
public get startedFileTasks(): PaperlessTask[] { takeUntil(this.unsubscribeNotifer),
return this.fileTasks().filter( catchError(() => EMPTY),
(t) => t.status === PaperlessTaskStatus.Started finalize(() => {
) this.loading = false
} })
)
public get completedFileTasks(): PaperlessTask[] { })
return this.fileTasks().filter( )
(t) => t.status === PaperlessTaskStatus.Success .subscribe((tasks) => {
) this.tasks.set(tasks)
} })
public get failedFileTasks(): PaperlessTask[] {
return this.fileTasks().filter(
(t) => t.status === PaperlessTaskStatus.Failure
)
} }
public get needsAttentionTasks(): PaperlessTask[] { public get needsAttentionTasks(): PaperlessTask[] {
return this.fileTasks().filter((t) => return this.tasks().filter((t) =>
[PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked].includes( [PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked].includes(
t.status t.status
) )
@@ -68,22 +72,7 @@ export class TasksService {
} }
public reload() { public reload() {
if (this.loading) return this.reloadNotifier.next()
this.loading = true
this.http
.get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
params: {
acknowledged: 'false',
page_size: this.defaultReloadPageSize,
},
})
.pipe(map((r) => r.results))
.pipe(takeUntil(this.unsubscribeNotifer), first())
.subscribe((r) => {
this.fileTasks.set(r)
this.loading = false
})
} }
public list( public list(
+2 -2
View File
@@ -1528,7 +1528,7 @@
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">306,308</context> <context context-type="linenumber">306,308</context>
</context-group> </context-group>
<target state="needs-translation"> Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI. These settings do not apply to documents. </target> <target state="translated"> 这些设置将应用于通过 Web 界面创建的对象(标签、邮件规则等)。这些设置不适用于文档。 </target>
</trans-unit> </trans-unit>
<trans-unit id="4292903881380648974" datatype="html"> <trans-unit id="4292903881380648974" datatype="html">
<source>Default Owner</source> <source>Default Owner</source>
@@ -9719,7 +9719,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<context context-type="linenumber">8</context> <context context-type="linenumber">8</context>
</context-group> </context-group>
<target state="needs-translation"><x id="INTERPOLATION" equiv-text="document().title | documentTitle }}"/> thumbnail</target> <target state="translated"><x id="INTERPOLATION" equiv-text="document().title | documentTitle }}"/> 缩略图</target>
</trans-unit> </trans-unit>
<trans-unit id="2784168796433474565" datatype="html"> <trans-unit id="2784168796433474565" datatype="html">
<source>Filter by tag</source> <source>Filter by tag</source>
+1 -1
View File
@@ -3,7 +3,7 @@ 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-27 19:34+0000\n" "POT-Creation-Date: 2026-07-27 19:34+0000\n"
"PO-Revision-Date: 2026-07-27 19:35\n" "PO-Revision-Date: 2026-07-28 12:46\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Chinese Simplified\n" "Language-Team: Chinese Simplified\n"
"Language: zh_CN\n" "Language: zh_CN\n"