Merge branch 'dev' into feature-schedule-wf-trigger

This commit is contained in:
shamoon
2024-11-14 20:47:19 -08:00
committed by GitHub
87 changed files with 4578 additions and 5417 deletions

View File

@@ -36,9 +36,7 @@
<ng-container i18n>Loading...</ng-container>
</div>
}
@for (log of logs; track log) {
<p
class="m-0 p-0 log-entry-{{getLogLevel(log)}}"
>{{log}}</p>
@for (log of logs; track $index) {
<p class="m-0 p-0 log-entry-{{getLogLevel(log)}}">{{log}}</p>
}
</div>

View File

@@ -118,17 +118,6 @@
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Document editor</span>
</div>
<div class="col">
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Sidebar</span>
@@ -168,26 +157,42 @@
<h4 class="mt-4" id="update-checking" i18n>Update checking</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<p i18n>
Update checking works by pinging the public <a href="https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest" target="_blank" rel="noopener noreferrer">GitHub API</a> for the latest release to determine whether a new version is available.<br/>
Actual updating of the app must still be performed manually.
</p>
<p i18n>
<em>No tracking data is collected by the app in any way.</em>
</p>
<div class="offset-md-3 col d-flex flex-row align-items-start">
<pngx-input-check i18n-title title="Enable update checking" formControlName="updateCheckingEnabled"></pngx-input-check>
<button class="btn btn-sm btn-link text-muted me-auto p-0 ms-2" title="What's this?" i18n-title type="button" triggers="mouseenter:mouseleave" [ngbPopover]="updatesPopover" [autoClose]="true">
<i-bs name="question-circle"></i-bs>
</button>
<ng-template #updatesPopover>
<p i18n>
Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually.
</p>
<p>
<em i18n>No tracking data is collected by the app in any way.</em>
</p>
</ng-template>
</div>
</div>
<h4 class="mt-4" i18n>Document editing</h4>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Use PDF viewer provided by the browser" i18n-hint hint="This is usually faster for displaying large PDF documents, but it might not work on some browsers." formControlName="useNativePdfViewer"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Automatically remove inbox tag(s) on save" formControlName="documentEditingRemoveInboxTags"></pngx-input-check>
</div>
</div>
<div class="row mb-3">
<div class="offset-md-3 col">
<pngx-input-check i18n-title title="Show document thumbnail during loading" formControlName="documentEditingOverlayThumbnail"></pngx-input-check>
</div>
</div>
<h4 class="mt-4" i18n>Bulk editing</h4>
<div class="row mb-3">

View File

@@ -315,7 +315,7 @@ describe('SettingsComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(27)
expect(setSpy).toHaveBeenCalledTimes(28)
// succeed
storeSpy.mockReturnValueOnce(of(true))

View File

@@ -88,7 +88,6 @@ export class SettingsComponent
darkModeEnabled: new FormControl(null),
darkModeInvertThumbs: new FormControl(null),
themeColor: new FormControl(null),
useNativePdfViewer: new FormControl(null),
displayLanguage: new FormControl(null),
dateLocale: new FormControl(null),
dateFormat: new FormControl(null),
@@ -99,7 +98,9 @@ export class SettingsComponent
defaultPermsViewGroups: new FormControl(null),
defaultPermsEditUsers: new FormControl(null),
defaultPermsEditGroups: new FormControl(null),
useNativePdfViewer: new FormControl(null),
documentEditingRemoveInboxTags: new FormControl(null),
documentEditingOverlayThumbnail: new FormControl(null),
searchDbOnly: new FormControl(null),
searchLink: new FormControl(null),
@@ -308,6 +309,9 @@ export class SettingsComponent
documentEditingRemoveInboxTags: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS
),
documentEditingOverlayThumbnail: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL
),
searchDbOnly: this.settings.get(SETTINGS_KEYS.SEARCH_DB_ONLY),
searchLink: this.settings.get(SETTINGS_KEYS.SEARCH_FULL_TYPE),
savedViews: {},
@@ -539,6 +543,10 @@ export class SettingsComponent
SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS,
this.settingsForm.value.documentEditingRemoveInboxTags
)
this.settings.set(
SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL,
this.settingsForm.value.documentEditingOverlayThumbnail
)
this.settings.set(
SETTINGS_KEYS.SEARCH_DB_ONLY,
this.settingsForm.value.searchDbOnly

View File

@@ -43,7 +43,7 @@
</tr>
</thead>
<tbody>
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task) {
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task.id) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">

View File

@@ -1,4 +1,4 @@
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow-sm">
<nav class="navbar navbar-dark fixed-top bg-primary flex-md-nowrap p-0 shadow-sm">
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
(click)="isMenuCollapsed = !isMenuCollapsed">

View File

@@ -48,6 +48,13 @@
main {
transition: all .2s ease;
padding-top: 110px;
}
@media (min-width: 768px) {
main {
padding-top: 56px;
}
}
.sidebar-slim-toggler {

View File

@@ -5,26 +5,26 @@
}
}
::ng-deep .ng-select-container {
:host ::ng-deep .ng-select-container {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
height: 100% !important;
}
::ng-deep .rounded-end .ng-select-container {
:host ::ng-deep .rounded-end .ng-select-container {
border-top-right-radius: var(--bs-border-radius) !important;
border-bottom-right-radius: var(--bs-border-radius) !important;
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
::ng-deep .ng-select {
:host ::ng-deep .ng-select {
max-width: 100px;
min-width: 35%;
font-size: 14px;
}
::ng-deep .doc-link-select {
:host ::ng-deep .doc-link-select {
padding-top: 0 !important;
border-top-right-radius: var(--bs-border-radius) !important;
border-bottom-right-radius: var(--bs-border-radius) !important;

View File

@@ -27,6 +27,9 @@
</div>
}
</div>
@if (object?.id) {
<small class="d-block mt-2" i18n>Warning: existing instances of this field will retain their current value index (e.g. option #1, #2, #3) after editing the options here</small>
}
}
@case (CustomFieldDataType.Monetary) {
<div class="my-3">

View File

@@ -38,7 +38,7 @@
@for (item of selectionModel.itemsSorted | filter: filterText:'name'; track item; let i = $index) {
@if (allowSelectNone || item.id) {
<pngx-toggleable-dropdown-button
[item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled">
[item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggled)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled">
</pngx-toggleable-dropdown-button>
}
}

View File

@@ -58,7 +58,7 @@ describe('ToggleableDropdownButtonComponent', () => {
let toggleResult
component.state = ToggleableItemState.Selected
component.exclude.subscribe(() => (excludeResult = true))
component.toggle.subscribe(() => (toggleResult = true))
component.toggled.subscribe(() => (toggleResult = true))
const button = fixture.nativeElement.querySelector('button')
button.dispatchEvent(new MouseEvent('click'))
expect(excludeResult).toBeTruthy()
@@ -70,7 +70,7 @@ describe('ToggleableDropdownButtonComponent', () => {
let toggleResult
component.state = ToggleableItemState.Excluded
component.exclude.subscribe(() => (excludeResult = true))
component.toggle.subscribe(() => (toggleResult = true))
component.toggled.subscribe(() => (toggleResult = true))
const button = fixture.nativeElement.querySelector('button')
button.dispatchEvent(new MouseEvent('click'))
expect(excludeResult).toBeFalsy()

View File

@@ -30,7 +30,7 @@ export class ToggleableDropdownButtonComponent {
hideCount: boolean = false
@Output()
toggle = new EventEmitter()
toggled = new EventEmitter()
@Output()
exclude = new EventEmitter()
@@ -43,7 +43,7 @@ export class ToggleableDropdownButtonComponent {
if (this.state == ToggleableItemState.Selected) {
this.exclude.emit()
} else {
this.toggle.emit()
this.toggled.emit()
}
}

View File

@@ -5,7 +5,11 @@
</div>
} @else {
@if (renderAsObject) {
<object [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object>
@if (previewText) {
<div class="bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{previewText}}</div>
} @else {
<object [data]="previewURL | safeUrl" width="100%" class="bg-light" [class.p-2]="!isPdf"></object>
}
} @else {
@if (requiresPassword) {
<div class="w-100 h-100 position-relative">

View File

@@ -7,32 +7,3 @@
::ng-deep .popover.popover-preview {
max-width: 32rem;
}
// https://github.com/paperless-ngx/paperless-ngx/issues/7920
// TODO: remove me
@mixin ff_txt {
.preview-popup-container {
width: 30rem !important;
height: 22rem !important;
background-color: #e7e7e7;
}
object {
mix-blend-mode: difference;
&.p-2 {
padding: 0 !important;
}
}
}
@-moz-document url-prefix() {
html[data-bs-theme='dark'] {
@include ff_txt;
}
html[data-bs-theme='auto'] {
@media screen and (prefers-color-scheme: dark) {
@include ff_txt;
}
}
}

View File

@@ -9,13 +9,20 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { DocumentService } from 'src/app/services/rest/document.service'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { PdfViewerModule } from 'ng2-pdf-viewer'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import {
HttpClient,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http'
import { of, throwError } from 'rxjs'
const doc = {
id: 10,
title: 'Document 10',
content: 'Cupcake ipsum dolor sit amet ice cream.',
original_file_name: 'sample.pdf',
archived_file_name: 'sample.pdf',
mime_type: 'application/pdf',
}
describe('PreviewPopupComponent', () => {
@@ -23,6 +30,7 @@ describe('PreviewPopupComponent', () => {
let fixture: ComponentFixture<PreviewPopupComponent>
let settingsService: SettingsService
let documentService: DocumentService
let http: HttpClient
beforeEach(() => {
TestBed.configureTestingModule({
@@ -35,23 +43,22 @@ describe('PreviewPopupComponent', () => {
})
settingsService = TestBed.inject(SettingsService)
documentService = TestBed.inject(DocumentService)
http = TestBed.inject(HttpClient)
jest
.spyOn(documentService, 'getPreviewUrl')
.mockImplementation((id) => doc.original_file_name)
fixture = TestBed.createComponent(PreviewPopupComponent)
component = fixture.componentInstance
component.document = doc
component.document = { ...doc }
fixture.detectChanges()
})
it('should guess if file is pdf by file name', () => {
expect(component.isPdf).toBeTruthy()
component.document.archived_file_name = 'sample.pdf'
it('should correctly report if document is pdf', () => {
expect(component.isPdf).toBeTruthy()
component.document.mime_type = 'application/msword'
expect(component.isPdf).toBeTruthy() // still has archive file
component.document.archived_file_name = undefined
component.document.original_file_name = 'sample.txt'
expect(component.isPdf).toBeFalsy()
component.document.original_file_name = 'sample.pdf'
})
it('should return settings for native PDF viewer', () => {
@@ -84,6 +91,8 @@ describe('PreviewPopupComponent', () => {
it('should fall back to object for non-pdf', () => {
component.document.original_file_name = 'sample.png'
component.document.mime_type = 'image/png'
component.document.archived_file_name = undefined
fixture.detectChanges()
expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
})
@@ -95,4 +104,22 @@ describe('PreviewPopupComponent', () => {
'Error loading preview'
)
})
it('should get text content from http if appropriate', () => {
component.document = {
...doc,
original_file_name: 'sample.txt',
mime_type: 'text/plain',
}
const httpSpy = jest.spyOn(http, 'get')
httpSpy.mockReturnValueOnce(
throwError(() => new Error('Error getting preview'))
)
component.init()
expect(httpSpy).toHaveBeenCalled()
expect(component.error).toBeTruthy()
httpSpy.mockReturnValueOnce(of('Preview text'))
component.init()
expect(component.previewText).toEqual('Preview text')
})
})

View File

@@ -1,4 +1,6 @@
import { Component, Input } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Component, Input, OnDestroy } from '@angular/core'
import { first, Subject, takeUntil } from 'rxjs'
import { Document } from 'src/app/data/document'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { DocumentService } from 'src/app/services/rest/document.service'
@@ -9,14 +11,26 @@ import { SettingsService } from 'src/app/services/settings.service'
templateUrl: './preview-popup.component.html',
styleUrls: ['./preview-popup.component.scss'],
})
export class PreviewPopupComponent {
export class PreviewPopupComponent implements OnDestroy {
private _document: Document
@Input()
document: Document
set document(document: Document) {
this._document = document
this.init()
}
get document(): Document {
return this._document
}
unsubscribeNotifier: Subject<any> = new Subject()
error = false
requiresPassword: boolean = false
previewText: string
get renderAsObject(): boolean {
return (this.isPdf && this.useNativePdfViewer) || !this.isPdf
}
@@ -30,18 +44,38 @@ export class PreviewPopupComponent {
}
get isPdf(): boolean {
// We dont have time to retrieve metadata, make a best guess by file name
return (
this.document?.original_file_name?.endsWith('.pdf') ||
this.document?.archived_file_name?.endsWith('.pdf')
this.document?.archived_file_name?.length > 0 ||
this.document?.mime_type?.includes('pdf')
)
}
constructor(
private settingsService: SettingsService,
private documentService: DocumentService
private documentService: DocumentService,
private http: HttpClient
) {}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(this)
}
init() {
if (this.document.mime_type?.includes('text')) {
this.http
.get(this.previewURL, { responseType: 'text' })
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (res) => {
this.previewText = res.toString()
},
error: (err) => {
this.error = err
},
})
}
}
onError(event: any) {
if (event.name == 'PasswordException') {
this.requiresPassword = true

View File

@@ -350,14 +350,16 @@
</ng-template>
<ng-template #previewContent>
@if (!metadata) {
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
<div class="thumb-preview position-absolute pe-none" [class.fade]="previewLoaded">
<img *ngIf="showThumbnailOverlay" [src]="thumbUrl | safeUrl" class="" width="100%" height="auto" alt="Document loading..." i18n-alt />
<div class="position-absolute top-0 start-0 m-2 p-2 d-flex align-items-center justify-content-center">
<div>
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
</div>
} @else {
</div>
@if (document) {
@switch (archiveContentRenderType) {
@case (ContentRenderType.PDF) {
@if (!useNativePdfViewer) {

View File

@@ -63,6 +63,27 @@ textarea.rtl {
object-fit: contain;
}
.whitespace-preserve {
white-space: preserve;
.thumb-preview {
top: 0;
left: 0;
width: 100%;
height: calc(100vh - 160px);
@media screen and (min-width: 768px) {
left: calc(.5 * var(--bs-gutter-x));
width: calc(100% - var(--bs-gutter-x));
}
overflow: hidden;
background-color: gray;
padding: 10px 8px; // border
z-index: 1000;
> div {
mix-blend-mode: difference;
}
> img {
filter: blur(1px);
}
}

View File

@@ -774,6 +774,15 @@ describe('DocumentDetailComponent', () => {
expect(component.previewNumPages).toEqual(1000)
})
it('should include delay of 300ms after previewloaded before showing pdf', fakeAsync(() => {
initNormally()
expect(component.previewLoaded).toBeFalsy()
component.pdfPreviewLoaded({ numPages: 1000 } as any)
expect(component.previewNumPages).toEqual(1000)
tick(300)
expect(component.previewLoaded).toBeTruthy()
}))
it('should support zoom controls', () => {
initNormally()
component.onZoomSelect({ target: { value: '1' } } as any) // from select
@@ -921,7 +930,7 @@ describe('DocumentDetailComponent', () => {
it('should display built-in pdf viewer if not disabled', () => {
initNormally()
component.metadata = { has_archive_version: true }
component.document.archived_file_name = 'file.pdf'
jest.spyOn(settingsService, 'get').mockReturnValue(false)
expect(component.useNativePdfViewer).toBeFalsy()
fixture.detectChanges()
@@ -930,7 +939,7 @@ describe('DocumentDetailComponent', () => {
it('should display native pdf viewer if enabled', () => {
initNormally()
component.metadata = { has_archive_version: true }
component.document.archived_file_name = 'file.pdf'
jest.spyOn(settingsService, 'get').mockReturnValue(true)
expect(component.useNativePdfViewer).toBeTruthy()
fixture.detectChanges()
@@ -1072,8 +1081,8 @@ describe('DocumentDetailComponent', () => {
})
it('should change preview element by render type', () => {
component.metadata = { has_archive_version: true }
initNormally()
component.document.archived_file_name = 'file.pdf'
fixture.detectChanges()
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.PDF
@@ -1082,10 +1091,8 @@ describe('DocumentDetailComponent', () => {
fixture.debugElement.query(By.css('pdf-viewer-container'))
).not.toBeUndefined()
component.metadata = {
has_archive_version: false,
original_mime_type: 'text/plain',
}
component.document.archived_file_name = undefined
component.document.mime_type = 'text/plain'
fixture.detectChanges()
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.Text
@@ -1094,10 +1101,7 @@ describe('DocumentDetailComponent', () => {
fixture.debugElement.query(By.css('div.preview-sticky'))
).not.toBeUndefined()
component.metadata = {
has_archive_version: false,
original_mime_type: 'image/jpg',
}
component.document.mime_type = 'image/jpeg'
fixture.detectChanges()
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.Image
@@ -1105,13 +1109,9 @@ describe('DocumentDetailComponent', () => {
expect(
fixture.debugElement.query(By.css('.preview-sticky img'))
).not.toBeUndefined()
component.metadata = {
has_archive_version: false,
original_mime_type:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}
fixture.detectChanges()
;(component.document.mime_type =
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'),
fixture.detectChanges()
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.Other
)
@@ -1221,6 +1221,14 @@ describe('DocumentDetailComponent', () => {
)
expect(saveSpy).toHaveBeenCalled()
jest.spyOn(openDocumentsService, 'isDirty').mockReturnValue(true)
jest.spyOn(component, 'hasNext').mockReturnValue(true)
const saveNextSpy = jest.spyOn(component, 'saveEditNext')
document.dispatchEvent(
new KeyboardEvent('keydown', { key: 's', ctrlKey: true, shiftKey: true })
)
expect(saveNextSpy).toHaveBeenCalled()
const closeSpy = jest.spyOn(component, 'close')
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'escape' }))
expect(closeSpy).toHaveBeenCalled()

View File

@@ -131,9 +131,11 @@ export class DocumentDetailComponent
title: string
titleSubject: Subject<string> = new Subject()
previewUrl: string
thumbUrl: string
previewText: string
downloadUrl: string
downloadOriginalUrl: string
previewLoaded: boolean = false
correspondents: Correspondent[]
documentTypes: DocumentType[]
@@ -221,15 +223,17 @@ export class DocumentDetailComponent
}
get archiveContentRenderType(): ContentRenderType {
return this.getRenderType(
this.metadata?.has_archive_version
? 'application/pdf'
: this.metadata?.original_mime_type
)
return this.document?.archived_file_name
? this.getRenderType('application/pdf')
: this.getRenderType(this.document?.mime_type)
}
get originalContentRenderType(): ContentRenderType {
return this.getRenderType(this.metadata?.original_mime_type)
return this.getRenderType(this.document?.mime_type)
}
get showThumbnailOverlay(): boolean {
return this.settings.get(SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL)
}
private getRenderType(mimeType: string): ContentRenderType {
@@ -339,6 +343,7 @@ export class DocumentDetailComponent
}`
},
})
this.thumbUrl = this.documentsService.getThumbUrl(documentId)
return this.documentsService.get(documentId)
})
)
@@ -504,6 +509,16 @@ export class DocumentDetailComponent
.subscribe(() => {
if (this.openDocumentService.isDirty(this.document)) this.save()
})
this.hotKeyService
.addShortcut({
keys: 'control.shift.s',
description: $localize`Save and close / next`,
})
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.openDocumentService.isDirty(this.document)) this.saveEditNext()
})
}
ngOnDestroy(): void {
@@ -537,6 +552,9 @@ export class DocumentDetailComponent
.subscribe({
next: (result) => {
this.metadata = result
if (this.archiveContentRenderType !== ContentRenderType.PDF) {
this.previewLoaded = true
}
},
error: (error) => {
this.metadata = {} // allow display to fallback to <object> tag
@@ -710,7 +728,10 @@ export class DocumentDetailComponent
next: (docValues) => {
// in case data changed while saving eg removing inbox_tags
this.documentForm.patchValue(docValues)
this.store.next(this.documentForm.value)
const newValues = Object.assign({}, this.documentForm.value)
newValues.tags = [...docValues.tags]
newValues.custom_fields = [...docValues.custom_fields]
this.store.next(newValues)
this.openDocumentService.setDirty(this.document, false)
this.openDocumentService.save()
this.toastService.showInfo($localize`Document saved successfully.`)
@@ -900,11 +921,15 @@ export class DocumentDetailComponent
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
this.previewNumPages = pdf.numPages
if (this.password) this.requiresPassword = false
setTimeout(() => {
this.previewLoaded = true
}, 300)
}
onError(event) {
if (event.name == 'PasswordException') {
this.requiresPassword = true
this.previewLoaded = true
}
}

View File

@@ -1,6 +1,6 @@
<div class="card mb-3 shadow-sm bg-light" [class.card-selected]="selected" [class.document-card]="selectable" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()">
<div class="row g-0">
<div class="col-md-2 doc-img-background rounded-start" [class.doc-img-background-selected]="selected" (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()">
<img [src]="getThumbUrl()" class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
<div class="border-end border-bottom bg-light document-card-check">

View File

@@ -2,7 +2,7 @@
overflow-wrap: anywhere;
}
.doc-img-background {
.doc-img-container {
position: relative;
}
@@ -11,7 +11,6 @@
object-position: top left;
height: 100%;
position: absolute;
mix-blend-mode: multiply;
}
.card-title {
@@ -49,18 +48,6 @@
display: block;
}
.card-selected {
border-color: var(--bs-primary);
.document-card-check {
display: block;
}
}
.doc-img-background-selected {
background-color: var(--pngx-primary-faded);
}
.card-info {
line-height: 1;

View File

@@ -1,7 +1,7 @@
<div class="col p-2 h-100">
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()">
<div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
<img class="card-img doc-img rounded-top" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
<div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
<img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
<div class="border-end border-bottom bg-light py-1 px-2 document-card-check">
<div class="form-check">

View File

@@ -6,7 +6,6 @@
object-fit: cover;
object-position: top left;
height: 180px;
mix-blend-mode: multiply;
}
.document-card-check {
@@ -40,18 +39,6 @@
top: 142px;
}
.card-selected {
border-color:var(--bs-primary);
.document-card-check {
display: block;
}
}
.doc-img-background-selected {
background-color: var(--pngx-primary-faded);
}
.card-info {
line-height: 1;

View File

@@ -1204,7 +1204,7 @@ describe('FilterEditorComponent', () => {
const tagButton = tagsFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)[0]
tagButton.triggerEventHandler('toggle')
tagButton.triggerEventHandler('toggled')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
@@ -1222,8 +1222,8 @@ describe('FilterEditorComponent', () => {
const tagButtons = tagsFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)
tagButtons[1].triggerEventHandler('toggle')
tagButtons[2].triggerEventHandler('toggle')
tagButtons[1].triggerEventHandler('toggled')
tagButtons[2].triggerEventHandler('toggled')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
@@ -1273,8 +1273,8 @@ describe('FilterEditorComponent', () => {
const correspondentButtons = correspondentsFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)
correspondentButtons[1].triggerEventHandler('toggle')
correspondentButtons[2].triggerEventHandler('toggle')
correspondentButtons[1].triggerEventHandler('toggled')
correspondentButtons[2].triggerEventHandler('toggled')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
@@ -1312,7 +1312,7 @@ describe('FilterEditorComponent', () => {
const notAssignedButton = correspondentsFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)[0]
notAssignedButton.triggerEventHandler('toggle')
notAssignedButton.triggerEventHandler('toggled')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
@@ -1330,8 +1330,8 @@ describe('FilterEditorComponent', () => {
const documentTypeButtons = documentTypesFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)
documentTypeButtons[1].triggerEventHandler('toggle')
documentTypeButtons[2].triggerEventHandler('toggle')
documentTypeButtons[1].triggerEventHandler('toggled')
documentTypeButtons[2].triggerEventHandler('toggled')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
@@ -1369,7 +1369,7 @@ describe('FilterEditorComponent', () => {
const notAssignedButton = docTypesFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)[0]
notAssignedButton.triggerEventHandler('toggle')
notAssignedButton.triggerEventHandler('toggled')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
@@ -1387,8 +1387,8 @@ describe('FilterEditorComponent', () => {
const storagePathButtons = storagePathFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)
storagePathButtons[1].triggerEventHandler('toggle')
storagePathButtons[2].triggerEventHandler('toggle')
storagePathButtons[1].triggerEventHandler('toggled')
storagePathButtons[2].triggerEventHandler('toggled')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
@@ -1426,7 +1426,7 @@ describe('FilterEditorComponent', () => {
const notAssignedButton = storagePathsFilterableDropdown.queryAll(
By.directive(ToggleableDropdownButtonComponent)
)[0]
notAssignedButton.triggerEventHandler('toggle')
notAssignedButton.triggerEventHandler('toggled')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{

View File

@@ -150,6 +150,8 @@ export interface Document extends ObjectWithPermissions {
added?: Date
mime_type?: string
deleted_at?: Date
original_file_name?: string

View File

@@ -61,6 +61,8 @@ export const SETTINGS_KEYS = {
DEFAULT_PERMS_EDIT_GROUPS: 'general-settings:permissions:default-edit-groups',
DOCUMENT_EDITING_REMOVE_INBOX_TAGS:
'general-settings:document-editing:remove-inbox-tags',
DOCUMENT_EDITING_OVERLAY_THUMBNAIL:
'general-settings:document-editing:overlay-thumbnail',
SEARCH_DB_ONLY: 'general-settings:search:db-only',
SEARCH_FULL_TYPE: 'general-settings:search:more-link',
EMPTY_TRASH_DELAY: 'trash_delay',
@@ -229,6 +231,11 @@ export const SETTINGS: UiSetting[] = [
type: 'boolean',
default: false,
},
{
key: SETTINGS_KEYS.DOCUMENT_EDITING_OVERLAY_THUMBNAIL,
type: 'boolean',
default: true,
},
{
key: SETTINGS_KEYS.SEARCH_DB_ONLY,
type: 'boolean',

View File

@@ -599,14 +599,17 @@ describe('DocumentListViewService', () => {
it('should not filter out custom fields if settings not initialized', () => {
const customFields = ['custom_field_1', 'custom_field_2']
documentListViewService.displayFields = customFields as any
settingsService.displayFieldsInitialized = false
expect(documentListViewService.displayFields).toEqual(customFields)
jest.spyOn(settingsService, 'allDisplayFields', 'get').mockReturnValue([
{ id: DisplayField.ADDED, name: 'Added' },
{ id: DisplayField.TITLE, name: 'Title' },
{ id: 'custom_field_1', name: 'Custom Field 1' },
] as any)
settingsService.displayFieldsInitialized = true
settingsService.displayFieldsInit.emit(true)
expect(documentListViewService.displayFields).toEqual(['custom_field_1'])
// will now filter on set
documentListViewService.displayFields = customFields as any
expect(documentListViewService.displayFields).toEqual(['custom_field_1'])
})
})

View File

@@ -20,6 +20,10 @@ import { paramsFromViewState, paramsToViewState } from '../utils/query-params'
import { DocumentService, SelectionData } from './rest/document.service'
import { SettingsService } from './settings.service'
const LIST_DEFAULT_DISPLAY_FIELDS: DisplayField[] = DEFAULT_DISPLAY_FIELDS.map(
(f) => f.id
).filter((f) => f !== DisplayField.ADDED)
/**
* Captures the current state of the list view.
*/
@@ -102,6 +106,8 @@ export class DocumentListViewService {
private _activeSavedViewId: number = null
private displayFieldsInitialized: boolean = false
get activeSavedViewId() {
return this._activeSavedViewId
}
@@ -134,6 +140,19 @@ export class DocumentListViewService {
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
}
}
this.settings.displayFieldsInit.subscribe(() => {
this.displayFieldsInitialized = true
if (this.activeListViewState.displayFields) {
this.activeListViewState.displayFields =
this.activeListViewState.displayFields.filter(
(field) =>
this.settings.allDisplayFields.find((f) => f.id === field) !==
undefined
)
this.saveDocumentListView()
}
})
}
private defaultListViewState(): ListViewState {
@@ -415,23 +434,17 @@ export class DocumentListViewService {
}
get displayFields(): DisplayField[] {
let fields =
this.activeListViewState.displayFields ??
DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
if (!this.activeListViewState.displayFields) {
fields = fields.filter((f) => f !== DisplayField.ADDED)
}
return this.settings.displayFieldsInitialized
? fields.filter(
return this.activeListViewState.displayFields ?? LIST_DEFAULT_DISPLAY_FIELDS
}
set displayFields(fields: DisplayField[]) {
this.activeListViewState.displayFields = this.displayFieldsInitialized
? fields?.filter(
(field) =>
this.settings.allDisplayFields.find((f) => f.id === field) !==
undefined
)
: fields
}
set displayFields(fields: DisplayField[]) {
this.activeListViewState.displayFields = fields
this.saveDocumentListView()
}

View File

@@ -274,7 +274,7 @@ export class SettingsService {
public get allDisplayFields(): Array<{ id: DisplayField; name: string }> {
return this._allDisplayFields
}
public displayFieldsInitialized: boolean = false
public displayFieldsInit: EventEmitter<boolean> = new EventEmitter()
constructor(
rendererFactory: RendererFactory2,
@@ -382,10 +382,10 @@ export class SettingsService {
}
})
)
this.displayFieldsInitialized = true
this.displayFieldsInit.emit(true)
})
} else {
this.displayFieldsInitialized = true
this.displayFieldsInit.emit(true)
}
}