mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-24 02:05:48 -06:00
Compare commits
13 Commits
v2.19.3
...
e9511bd3da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9511bd3da | ||
|
|
8b9ca75a90 | ||
|
|
9f0a4ac19d | ||
|
|
8f969ecab5 | ||
|
|
245e52a4eb | ||
|
|
a8c75d95d8 | ||
|
|
d6e2456baf | ||
|
|
3b75d3271e | ||
|
|
e88816d141 | ||
|
|
e5bd4713ac | ||
|
|
b9aced07fb | ||
|
|
6b55740f56 | ||
|
|
9aee063347 |
@@ -1,5 +1,42 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 2.19.3
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: remove unnecessary permission requirements for new email endpoint [@shamoon](https://github.com/shamoon) ([#11215](https://github.com/paperless-ngx/paperless-ngx/pull/11215))
|
||||||
|
- Fix: refactor nested sorting in filterable dropdowns [@shamoon](https://github.com/shamoon) ([#11214](https://github.com/paperless-ngx/paperless-ngx/pull/11214))
|
||||||
|
- Fix: add root tag filtering for tag list page consistency, fix toggle all [@shamoon](https://github.com/shamoon) ([#11208](https://github.com/paperless-ngx/paperless-ngx/pull/11208))
|
||||||
|
- Fix: support ConsumableDocument in email attachments [@shamoon](https://github.com/shamoon) ([#11196](https://github.com/paperless-ngx/paperless-ngx/pull/11196))
|
||||||
|
- Fix: add missing import for ConfirmButtonComponent in user-edit-dialog [@shamoon](https://github.com/shamoon) ([#11167](https://github.com/paperless-ngx/paperless-ngx/pull/11167))
|
||||||
|
- Fix: resolve migration warning in 2.19.2 [@shamoon](https://github.com/shamoon) ([#11157](https://github.com/paperless-ngx/paperless-ngx/pull/11157))
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Change: make workflow action only title draggable [@shamoon](https://github.com/shamoon) ([#11209](https://github.com/paperless-ngx/paperless-ngx/pull/11209))
|
||||||
|
- Change: change workflowrun to softdeletemodel [@shamoon](https://github.com/shamoon) ([#11194](https://github.com/paperless-ngx/paperless-ngx/pull/11194))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- Chore(deps): Bump django from 5.2.6 to 5.2.7 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11200](https://github.com/paperless-ngx/paperless-ngx/pull/11200))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>9 changes</summary>
|
||||||
|
|
||||||
|
- Chore(deps): Bump django from 5.2.6 to 5.2.7 @[dependabot[bot]](https://github.com/apps/dependabot) ([#11200](https://github.com/paperless-ngx/paperless-ngx/pull/11200))
|
||||||
|
- Fix: remove unnecessary permission requirements for new email endpoint [@shamoon](https://github.com/shamoon) ([#11215](https://github.com/paperless-ngx/paperless-ngx/pull/11215))
|
||||||
|
- Fix: refactor nested sorting in filterable dropdowns [@shamoon](https://github.com/shamoon) ([#11214](https://github.com/paperless-ngx/paperless-ngx/pull/11214))
|
||||||
|
- Fix: add root tag filtering for tag list page consistency, fix toggle all [@shamoon](https://github.com/shamoon) ([#11208](https://github.com/paperless-ngx/paperless-ngx/pull/11208))
|
||||||
|
- Change: make workflow action only title draggable [@shamoon](https://github.com/shamoon) ([#11209](https://github.com/paperless-ngx/paperless-ngx/pull/11209))
|
||||||
|
- Change: change workflowrun to softdeletemodel [@shamoon](https://github.com/shamoon) ([#11194](https://github.com/paperless-ngx/paperless-ngx/pull/11194))
|
||||||
|
- Chore: Minor migration optimization for workflow titles [@stumpylog](https://github.com/stumpylog) ([#11197](https://github.com/paperless-ngx/paperless-ngx/pull/11197))
|
||||||
|
- Fix: support ConsumableDocument in email attachments [@shamoon](https://github.com/shamoon) ([#11196](https://github.com/paperless-ngx/paperless-ngx/pull/11196))
|
||||||
|
- Fix: add missing import for ConfirmButtonComponent in user-edit-dialog [@shamoon](https://github.com/shamoon) ([#11167](https://github.com/paperless-ngx/paperless-ngx/pull/11167))
|
||||||
|
- Fix: resolve migration warning in 2.19.2 [@shamoon](https://github.com/shamoon) ([#11157](https://github.com/paperless-ngx/paperless-ngx/pull/11157))
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 2.19.2
|
## paperless-ngx 2.19.2
|
||||||
|
|
||||||
### Features / Enhancements
|
### Features / Enhancements
|
||||||
|
|||||||
@@ -691,7 +691,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
<context context-type="linenumber">36</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
@@ -7259,25 +7259,25 @@
|
|||||||
<source>Print failed.</source>
|
<source>Print failed.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1455</context>
|
<context context-type="linenumber">1460</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6457245677384603573" datatype="html">
|
<trans-unit id="6457245677384603573" datatype="html">
|
||||||
<source>Error loading document for printing.</source>
|
<source>Error loading document for printing.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1463</context>
|
<context context-type="linenumber">1472</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6085793215710522488" datatype="html">
|
<trans-unit id="6085793215710522488" datatype="html">
|
||||||
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
<source>An error occurred loading tiff: <x id="PH" equiv-text="err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1528</context>
|
<context context-type="linenumber">1537</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">1532</context>
|
<context context-type="linenumber">1541</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4958946940233632319" datatype="html">
|
<trans-unit id="4958946940233632319" datatype="html">
|
||||||
|
|||||||
@@ -29,14 +29,19 @@
|
|||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||||
|
|
||||||
<div class="bg-dark p-3 text-light font-monospace log-container" #logContainer>
|
<cdk-virtual-scroll-viewport
|
||||||
|
itemSize="20"
|
||||||
|
class="bg-dark p-3 text-light font-monospace log-container"
|
||||||
|
#logContainer>
|
||||||
@if (loading && logFiles.length) {
|
@if (loading && logFiles.length) {
|
||||||
<div>
|
<div>
|
||||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
<ng-container i18n>Loading...</ng-container>
|
<ng-container i18n>Loading...</ng-container>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@for (log of logs; track $index) {
|
<p *cdkVirtualFor="let log of logs"
|
||||||
<p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
|
class="m-0 p-0"
|
||||||
}
|
[ngClass]="'log-entry-' + log.level">
|
||||||
</div>
|
{{log.message}}
|
||||||
|
</p>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
.log-container {
|
.log-container {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
height: calc(100vh - 200px);
|
height: calc(100vh - 200px);
|
||||||
top: 70px;
|
top: 0;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
CdkVirtualScrollViewport,
|
||||||
|
ScrollingModule,
|
||||||
|
} from '@angular/cdk/scrolling'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
import { 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'
|
||||||
@@ -38,6 +43,9 @@ describe('LogsComponent', () => {
|
|||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
LogsComponent,
|
LogsComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
|
CommonModule,
|
||||||
|
CdkVirtualScrollViewport,
|
||||||
|
ScrollingModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
@@ -54,7 +62,6 @@ describe('LogsComponent', () => {
|
|||||||
fixture = TestBed.createComponent(LogsComponent)
|
fixture = TestBed.createComponent(LogsComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
reloadSpy = jest.spyOn(component, 'reloadLogs')
|
reloadSpy = jest.spyOn(component, 'reloadLogs')
|
||||||
window.HTMLElement.prototype.scroll = function () {} // mock scroll
|
|
||||||
jest.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
})
|
})
|
||||||
@@ -83,6 +90,10 @@ describe('LogsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should auto refresh, allow toggle', () => {
|
it('should auto refresh, allow toggle', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(CdkVirtualScrollViewport.prototype, 'scrollToIndex')
|
||||||
|
.mockImplementation(() => undefined)
|
||||||
|
|
||||||
jest.advanceTimersByTime(6000)
|
jest.advanceTimersByTime(6000)
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
import {
|
||||||
|
CdkVirtualScrollViewport,
|
||||||
|
ScrollingModule,
|
||||||
|
} from '@angular/cdk/scrolling'
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
@@ -21,8 +25,11 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
imports: [
|
imports: [
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
|
CdkVirtualScrollViewport,
|
||||||
|
ScrollingModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class LogsComponent
|
export class LogsComponent
|
||||||
@@ -32,7 +39,7 @@ export class LogsComponent
|
|||||||
private logService = inject(LogService)
|
private logService = inject(LogService)
|
||||||
private changedetectorRef = inject(ChangeDetectorRef)
|
private changedetectorRef = inject(ChangeDetectorRef)
|
||||||
|
|
||||||
public logs: string[] = []
|
public logs: Array<{ message: string; level: number }> = []
|
||||||
|
|
||||||
public logFiles: string[] = []
|
public logFiles: string[] = []
|
||||||
|
|
||||||
@@ -40,7 +47,7 @@ export class LogsComponent
|
|||||||
|
|
||||||
public autoRefreshEnabled: boolean = true
|
public autoRefreshEnabled: boolean = true
|
||||||
|
|
||||||
@ViewChild('logContainer') logContainer: ElementRef
|
@ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.logService
|
this.logService
|
||||||
@@ -75,7 +82,7 @@ export class LogsComponent
|
|||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
this.logs = result
|
this.logs = this.parseLogsWithLevel(result)
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.scrollToBottom()
|
this.scrollToBottom()
|
||||||
},
|
},
|
||||||
@@ -100,12 +107,19 @@ export class LogsComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseLogsWithLevel(
|
||||||
|
logs: string[]
|
||||||
|
): Array<{ message: string; level: number }> {
|
||||||
|
return logs.map((log) => ({
|
||||||
|
message: log,
|
||||||
|
level: this.getLogLevel(log),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
scrollToBottom(): void {
|
scrollToBottom(): void {
|
||||||
this.changedetectorRef.detectChanges()
|
this.changedetectorRef.detectChanges()
|
||||||
this.logContainer?.nativeElement.scroll({
|
if (this.logContainer) {
|
||||||
top: this.logContainer.nativeElement.scrollHeight,
|
this.logContainer.scrollToIndex(this.logs.length - 1)
|
||||||
left: 0,
|
}
|
||||||
behavior: 'auto',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1489,6 +1489,8 @@ describe('DocumentDetailComponent', () => {
|
|||||||
mockContentWindow.onafterprint(new Event('afterprint'))
|
mockContentWindow.onafterprint(new Event('afterprint'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tick(500)
|
||||||
|
|
||||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
||||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
|
|
||||||
@@ -1512,65 +1514,97 @@ describe('DocumentDetailComponent', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show error toast if printing throws inside iframe', fakeAsync(() => {
|
const iframePrintErrorCases: Array<{
|
||||||
initNormally()
|
description: string
|
||||||
|
thrownError: Error
|
||||||
|
expectToast: boolean
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
description: 'should show error toast if printing throws inside iframe',
|
||||||
|
thrownError: new Error('focus failed'),
|
||||||
|
expectToast: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'should suppress toast if cross-origin afterprint error occurs',
|
||||||
|
thrownError: new DOMException(
|
||||||
|
'Accessing onafterprint triggered a cross-origin violation',
|
||||||
|
'SecurityError'
|
||||||
|
),
|
||||||
|
expectToast: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const appendChildSpy = jest
|
iframePrintErrorCases.forEach(({ description, thrownError, expectToast }) => {
|
||||||
.spyOn(document.body, 'appendChild')
|
it(
|
||||||
.mockImplementation((node: Node) => node)
|
description,
|
||||||
const removeChildSpy = jest
|
fakeAsync(() => {
|
||||||
.spyOn(document.body, 'removeChild')
|
initNormally()
|
||||||
.mockImplementation((node: Node) => node)
|
|
||||||
const createObjectURLSpy = jest
|
|
||||||
.spyOn(URL, 'createObjectURL')
|
|
||||||
.mockReturnValue('blob:mock-url')
|
|
||||||
const revokeObjectURLSpy = jest
|
|
||||||
.spyOn(URL, 'revokeObjectURL')
|
|
||||||
.mockImplementation(() => {})
|
|
||||||
|
|
||||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
const appendChildSpy = jest
|
||||||
|
.spyOn(document.body, 'appendChild')
|
||||||
|
.mockImplementation((node: Node) => node)
|
||||||
|
const removeChildSpy = jest
|
||||||
|
.spyOn(document.body, 'removeChild')
|
||||||
|
.mockImplementation((node: Node) => node)
|
||||||
|
const createObjectURLSpy = jest
|
||||||
|
.spyOn(URL, 'createObjectURL')
|
||||||
|
.mockReturnValue('blob:mock-url')
|
||||||
|
const revokeObjectURLSpy = jest
|
||||||
|
.spyOn(URL, 'revokeObjectURL')
|
||||||
|
.mockImplementation(() => {})
|
||||||
|
|
||||||
const mockContentWindow = {
|
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||||
focus: jest.fn().mockImplementation(() => {
|
|
||||||
throw new Error('focus failed')
|
|
||||||
}),
|
|
||||||
print: jest.fn(),
|
|
||||||
onafterprint: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockIframe: any = {
|
const mockContentWindow = {
|
||||||
style: {},
|
focus: jest.fn().mockImplementation(() => {
|
||||||
src: '',
|
throw thrownError
|
||||||
onload: null,
|
}),
|
||||||
contentWindow: mockContentWindow,
|
print: jest.fn(),
|
||||||
}
|
onafterprint: null,
|
||||||
|
}
|
||||||
|
|
||||||
const createElementSpy = jest
|
const mockIframe: any = {
|
||||||
.spyOn(document, 'createElement')
|
style: {},
|
||||||
.mockReturnValue(mockIframe as any)
|
src: '',
|
||||||
|
onload: null,
|
||||||
|
contentWindow: mockContentWindow,
|
||||||
|
}
|
||||||
|
|
||||||
const blob = new Blob(['test'], { type: 'application/pdf' })
|
const createElementSpy = jest
|
||||||
component.printDocument()
|
.spyOn(document, 'createElement')
|
||||||
|
.mockReturnValue(mockIframe as any)
|
||||||
|
|
||||||
const req = httpTestingController.expectOne(
|
const blob = new Blob(['test'], { type: 'application/pdf' })
|
||||||
`${environment.apiBaseUrl}documents/${doc.id}/download/`
|
component.printDocument()
|
||||||
|
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/${doc.id}/download/`
|
||||||
|
)
|
||||||
|
req.flush(blob)
|
||||||
|
|
||||||
|
tick()
|
||||||
|
|
||||||
|
if (mockIframe.onload) {
|
||||||
|
mockIframe.onload(new Event('load'))
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(200)
|
||||||
|
|
||||||
|
if (expectToast) {
|
||||||
|
expect(toastSpy).toHaveBeenCalled()
|
||||||
|
} else {
|
||||||
|
expect(toastSpy).not.toHaveBeenCalled()
|
||||||
|
}
|
||||||
|
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
||||||
|
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
||||||
|
|
||||||
|
createElementSpy.mockRestore()
|
||||||
|
appendChildSpy.mockRestore()
|
||||||
|
removeChildSpy.mockRestore()
|
||||||
|
createObjectURLSpy.mockRestore()
|
||||||
|
revokeObjectURLSpy.mockRestore()
|
||||||
|
})
|
||||||
)
|
)
|
||||||
req.flush(blob)
|
})
|
||||||
|
|
||||||
tick()
|
|
||||||
|
|
||||||
if (mockIframe.onload) {
|
|
||||||
mockIframe.onload(new Event('load'))
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(toastSpy).toHaveBeenCalled()
|
|
||||||
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
|
|
||||||
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
|
|
||||||
|
|
||||||
createElementSpy.mockRestore()
|
|
||||||
appendChildSpy.mockRestore()
|
|
||||||
removeChildSpy.mockRestore()
|
|
||||||
createObjectURLSpy.mockRestore()
|
|
||||||
revokeObjectURLSpy.mockRestore()
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
|||||||
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||||
import { BehaviorSubject, Observable, of, Subject } from 'rxjs'
|
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
catchError,
|
catchError,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
@@ -1452,9 +1452,18 @@ export class DocumentDetailComponent
|
|||||||
URL.revokeObjectURL(blobUrl)
|
URL.revokeObjectURL(blobUrl)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.toastService.showError($localize`Print failed.`, err)
|
// FF throws cross-origin error on onafterprint
|
||||||
document.body.removeChild(iframe)
|
const isCrossOriginAfterPrintError =
|
||||||
URL.revokeObjectURL(blobUrl)
|
err instanceof DOMException &&
|
||||||
|
err.message.includes('onafterprint')
|
||||||
|
if (!isCrossOriginAfterPrintError) {
|
||||||
|
this.toastService.showError($localize`Print failed.`, err)
|
||||||
|
}
|
||||||
|
timer(100).subscribe(() => {
|
||||||
|
// delay to avoid FF print failure
|
||||||
|
document.body.removeChild(iframe)
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,9 +73,14 @@ describe('TagListComponent', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should filter out child tags if name filter is empty, otherwise show all', () => {
|
it('should omit matching children from top level when their parent is present', () => {
|
||||||
const tags = [
|
const tags = [
|
||||||
{ id: 1, name: 'Tag1', parent: null },
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Tag1',
|
||||||
|
parent: null,
|
||||||
|
children: [{ id: 2, name: 'Tag2', parent: 1 }],
|
||||||
|
},
|
||||||
{ id: 2, name: 'Tag2', parent: 1 },
|
{ id: 2, name: 'Tag2', parent: 1 },
|
||||||
{ id: 3, name: 'Tag3', parent: null },
|
{ id: 3, name: 'Tag3', parent: null },
|
||||||
]
|
]
|
||||||
@@ -86,7 +91,13 @@ describe('TagListComponent', () => {
|
|||||||
|
|
||||||
component['_nameFilter'] = 'Tag2' // Simulate non-empty name filter
|
component['_nameFilter'] = 'Tag2' // Simulate non-empty name filter
|
||||||
const filteredWithName = component.filterData(tags as any)
|
const filteredWithName = component.filterData(tags as any)
|
||||||
expect(filteredWithName.length).toBe(3)
|
expect(filteredWithName.length).toBe(2)
|
||||||
|
expect(filteredWithName.find((t) => t.id === 2)).toBeUndefined()
|
||||||
|
expect(
|
||||||
|
filteredWithName
|
||||||
|
.find((t) => t.id === 1)
|
||||||
|
?.children?.some((c) => c.id === 2)
|
||||||
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should request only parent tags when no name filter is applied', () => {
|
it('should request only parent tags when no name filter is applied', () => {
|
||||||
|
|||||||
@@ -69,9 +69,13 @@ export class TagListComponent extends ManagementListComponent<Tag> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filterData(data: Tag[]) {
|
filterData(data: Tag[]) {
|
||||||
return this.nameFilter?.length
|
if (!this.nameFilter?.length) {
|
||||||
? [...data]
|
return data.filter((tag) => !tag.parent)
|
||||||
: data.filter((tag) => !tag.parent)
|
}
|
||||||
|
|
||||||
|
// When filtering by name, exclude children if their parent is also present
|
||||||
|
const availableIds = new Set(data.map((tag) => tag.id))
|
||||||
|
return data.filter((tag) => !tag.parent || !availableIds.has(tag.parent))
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override getSelectableIDs(tags: Tag[]): number[] {
|
protected override getSelectableIDs(tags: Tag[]): number[] {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.core.cache import cache
|
||||||
from pytest_httpx import HTTPXMock
|
from pytest_httpx import HTTPXMock
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient
|
from rest_framework.test import APIClient
|
||||||
@@ -8,6 +9,9 @@ from paperless import version
|
|||||||
class TestApiRemoteVersion:
|
class TestApiRemoteVersion:
|
||||||
ENDPOINT = "/api/remote_version/"
|
ENDPOINT = "/api/remote_version/"
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
def test_remote_version_enabled_no_update_prefix(
|
def test_remote_version_enabled_no_update_prefix(
|
||||||
self,
|
self,
|
||||||
rest_api_client: APIClient,
|
rest_api_client: APIClient,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ from django.utils.timezone import make_aware
|
|||||||
from django.utils.translation import get_language
|
from django.utils.translation import get_language
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.cache import cache_control
|
from django.views.decorators.cache import cache_control
|
||||||
|
from django.views.decorators.cache import cache_page
|
||||||
from django.views.decorators.http import condition
|
from django.views.decorators.http import condition
|
||||||
from django.views.decorators.http import last_modified
|
from django.views.decorators.http import last_modified
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
@@ -2402,6 +2403,7 @@ class UiSettingsView(GenericAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(cache_page(60 * 15), name="dispatch")
|
||||||
@extend_schema_view(
|
@extend_schema_view(
|
||||||
get=extend_schema(
|
get=extend_schema(
|
||||||
description="Get the current version of the Paperless-NGX server",
|
description="Get the current version of the Paperless-NGX server",
|
||||||
|
|||||||
Reference in New Issue
Block a user