mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-01 20:45:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61ad1ba479 | ||
|
|
5b63c9a0ff | ||
|
|
217c5e681d |
No files matched your search
@@ -38,7 +38,7 @@ repos:
|
|||||||
- json
|
- json
|
||||||
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
|
||||||
- repo: https://github.com/rbubley/mirrors-prettier
|
- repo: https://github.com/rbubley/mirrors-prettier
|
||||||
rev: 'v3.9.9'
|
rev: 'v3.9.10'
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
types_or:
|
types_or:
|
||||||
@@ -46,16 +46,16 @@ repos:
|
|||||||
- ts
|
- ts
|
||||||
- markdown
|
- markdown
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- prettier@3.9.9
|
- prettier@3.9.10
|
||||||
- 'prettier-plugin-organize-imports@4.3.0'
|
- 'prettier-plugin-organize-imports@4.3.0'
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.16.9
|
rev: v0.17.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
rev: "v2.29.4"
|
rev: "v2.30.3"
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyproject-fmt
|
- id: pyproject-fmt
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
|
|||||||
@@ -485,7 +485,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="input-group input-group-sm w-auto flex-nowrap">
|
<div class="input-group input-group-sm w-auto flex-nowrap">
|
||||||
<button class="btn btn-outline-secondary" (click)="decreaseZoom()" aria-label="Zoom out" i18n-aria-label>
|
<button class="btn btn-outline-secondary" type="button" (click)="decreaseZoom()" aria-label="Zoom out" i18n-aria-label>
|
||||||
<i-bs width="1em" height="1em" name="dash"></i-bs>
|
<i-bs width="1em" height="1em" name="dash"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
<select class="form-select" (change)="setZoom($event.target.value)" [ngModelOptions]="{standalone: true}" [ngModel]="currentZoom" aria-label="Zoom" i18n-aria-label>
|
<select class="form-select" (change)="setZoom($event.target.value)" [ngModelOptions]="{standalone: true}" [ngModel]="currentZoom" aria-label="Zoom" i18n-aria-label>
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
</option>
|
</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
<button class="btn btn-outline-secondary" (click)="increaseZoom()" aria-label="Zoom in" i18n-aria-label>
|
<button class="btn btn-outline-secondary" type="button" (click)="increaseZoom()" aria-label="Zoom in" i18n-aria-label>
|
||||||
<i-bs width="1em" height="1em" name="plus"></i-bs>
|
<i-bs width="1em" height="1em" name="plus"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
@@ -47,6 +47,8 @@
|
|||||||
<span>{{ change.key | titlecase }}</span>:
|
<span>{{ change.key | titlecase }}</span>:
|
||||||
@if (change.key === 'content') {
|
@if (change.key === 'content') {
|
||||||
<code class="text-primary">{{ change.value[1]?.substring(0,100) }}...</code>
|
<code class="text-primary">{{ change.value[1]?.substring(0,100) }}...</code>
|
||||||
|
} @else if (change.key === 'tags') {
|
||||||
|
<code class="text-primary">{{ getTagNames($any(change.value[1])) | async }}</code>
|
||||||
} @else {
|
} @else {
|
||||||
<code class="text-primary">{{ getPrettyName(change.key, change.value[1]) | async }}</code>
|
<code class="text-primary">{{ getPrettyName(change.key, change.value[1]) | async }}</code>
|
||||||
}
|
}
|
||||||
|
|||||||
+34
@@ -12,6 +12,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
|||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { DocumentHistoryComponent } from './document-history.component'
|
import { DocumentHistoryComponent } from './document-history.component'
|
||||||
|
|
||||||
@@ -174,6 +175,39 @@ describe('DocumentHistoryComponent', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should render bulk tag history with names and preserve missing tag IDs', () => {
|
||||||
|
const getCachedSpy = jest
|
||||||
|
.spyOn(TestBed.inject(TagService), 'getCached')
|
||||||
|
.mockImplementation((id) =>
|
||||||
|
of(id === 1 ? { id: 1, name: 'Invoice' } : null)
|
||||||
|
)
|
||||||
|
component.entries.set([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
action: AuditLogAction.Update,
|
||||||
|
timestamp: '2021-01-01T00:00:00Z',
|
||||||
|
remote_addr: null,
|
||||||
|
changes: JSON.parse('{"tags":[[],[1,2345]]}'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
component.loading.set(false)
|
||||||
|
fixture.detectChanges()
|
||||||
|
|
||||||
|
expect(fixture.nativeElement.querySelector('code').textContent).toBe(
|
||||||
|
'Invoice, 2345'
|
||||||
|
)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(getCachedSpy).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle removal of all tags without a lookup', () => {
|
||||||
|
const getCachedSpy = jest.spyOn(TestBed.inject(TagService), 'getCached')
|
||||||
|
component.getTagNames([]).subscribe((result) => {
|
||||||
|
expect(result).toBe('')
|
||||||
|
})
|
||||||
|
expect(getCachedSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('getPrettyName should return the value as is for other types', () => {
|
it('getPrettyName should return the value as is for other types', () => {
|
||||||
const id = '123'
|
const id = '123'
|
||||||
component.getPrettyName('other', id).subscribe((result) => {
|
component.getPrettyName('other', id).subscribe((result) => {
|
||||||
|
|||||||
+27
-1
@@ -2,7 +2,7 @@ import { AsyncPipe, KeyValuePipe, TitleCasePipe } from '@angular/common'
|
|||||||
import { Component, Input, OnInit, inject, signal } from '@angular/core'
|
import { Component, Input, OnInit, inject, signal } from '@angular/core'
|
||||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { Observable, first, map, of, shareReplay } from 'rxjs'
|
import { Observable, first, forkJoin, map, of, shareReplay } from 'rxjs'
|
||||||
import { AuditLogAction, AuditLogEntry } from 'src/app/data/auditlog-entry'
|
import { AuditLogAction, AuditLogEntry } from 'src/app/data/auditlog-entry'
|
||||||
import { DataType } from 'src/app/data/datatype'
|
import { DataType } from 'src/app/data/datatype'
|
||||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||||
@@ -10,6 +10,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
|||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||||
|
import { TagService } from 'src/app/services/rest/tag.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -30,6 +31,7 @@ export class DocumentHistoryComponent implements OnInit {
|
|||||||
private readonly correspondentService = inject(CorrespondentService)
|
private readonly correspondentService = inject(CorrespondentService)
|
||||||
private readonly storagePathService = inject(StoragePathService)
|
private readonly storagePathService = inject(StoragePathService)
|
||||||
private readonly documentTypeService = inject(DocumentTypeService)
|
private readonly documentTypeService = inject(DocumentTypeService)
|
||||||
|
private readonly tagService = inject(TagService)
|
||||||
private readonly userService = inject(UserService)
|
private readonly userService = inject(UserService)
|
||||||
|
|
||||||
public AuditLogAction = AuditLogAction
|
public AuditLogAction = AuditLogAction
|
||||||
@@ -63,6 +65,30 @@ export class DocumentHistoryComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTagNames(tagIds: number[]): Observable<string> {
|
||||||
|
const cacheKey = `tags:${JSON.stringify(tagIds)}`
|
||||||
|
const cachedNames = this.prettyNameCache.get(cacheKey)
|
||||||
|
if (cachedNames) {
|
||||||
|
return cachedNames
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagNames$ = tagIds.length
|
||||||
|
? forkJoin(
|
||||||
|
tagIds.map((tagId) =>
|
||||||
|
this.tagService.getCached(tagId).pipe(
|
||||||
|
first(),
|
||||||
|
map((tag) => tag?.name ?? String(tagId))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).pipe(map((names) => names.join(', ')))
|
||||||
|
: of('')
|
||||||
|
const names$ = tagNames$.pipe(
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true })
|
||||||
|
)
|
||||||
|
this.prettyNameCache.set(cacheKey, names$)
|
||||||
|
return names$
|
||||||
|
}
|
||||||
|
|
||||||
getPrettyName(type: DataType | string, id: string): Observable<string> {
|
getPrettyName(type: DataType | string, id: string): Observable<string> {
|
||||||
const cacheKey = `${type}:${id}`
|
const cacheKey = `${type}:${id}`
|
||||||
const cached = this.prettyNameCache.get(cacheKey)
|
const cached = this.prettyNameCache.get(cacheKey)
|
||||||
|
|||||||
Reference in new issue
Block a user