Just save this

[ci skip]
This commit is contained in:
shamoon 2025-01-22 12:13:35 -08:00
parent 2c28348b56
commit 4070cd0e1b
8 changed files with 422 additions and 272 deletions

View File

@ -1,10 +1,14 @@
<a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle"
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body"
autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<ng-content></ng-content>
</a>
@if (!previewOnly) {
<a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle"
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body"
autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<ng-content></ng-content>
</a>
} @else {
<ng-container [ngTemplateOutlet]="previewContent" [ngTemplateOutletContext]="{ $implicit: document }"></ng-container>
}
<ng-template #previewContent>
<div class="preview-popup-container" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview(); close()">
<div class="preview-popup-container" [class.full-size]="previewOnly" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview(); close()">
@if (error) {
<div class="w-100 h-100 position-relative">
<p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p>

View File

@ -4,6 +4,16 @@
overflow-y: scroll;
}
.preview-popup-container.full-size {
width: 100% !important;
height: 100% !important;
> * {
width: 100% !important;
height: 100% !important;
}
}
::ng-deep .popover.popover-preview {
max-width: 32rem;
}

View File

@ -1,3 +1,4 @@
import { NgTemplateOutlet } from '@angular/common'
import { HttpClient } from '@angular/common/http'
import { Component, Input, OnDestroy, ViewChild } from '@angular/core'
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
@ -17,6 +18,7 @@ import { SettingsService } from 'src/app/services/settings.service'
styleUrls: ['./preview-popup.component.scss'],
imports: [
NgbPopoverModule,
NgTemplateOutlet,
DocumentTitlePipe,
PdfViewerModule,
SafeUrlPipe,
@ -47,6 +49,9 @@ export class PreviewPopupComponent implements OnDestroy {
@Input()
linkTitle: string = $localize`Open preview`
@Input()
previewOnly: boolean = false
unsubscribeNotifier: Subject<any> = new Subject()
error = false
@ -91,6 +96,8 @@ export class PreviewPopupComponent implements OnDestroy {
}
init() {
this.error = false
this.requiresPassword = false
if (this.document.mime_type?.includes('text')) {
this.http
.get(this.previewURL, { responseType: 'text' })
@ -119,6 +126,7 @@ export class PreviewPopupComponent implements OnDestroy {
}
mouseEnterPreview() {
if (this.previewOnly) return
this.mouseOnPreview = true
if (!this.popover.isOpen()) {
// we're going to open but hide to pre-load content during hover delay
@ -136,10 +144,12 @@ export class PreviewPopupComponent implements OnDestroy {
}
mouseLeavePreview() {
if (this.previewOnly) return
this.mouseOnPreview = false
}
public close(immediate: boolean = false) {
if (this.previewOnly) return
setTimeout(
() => {
if (!this.mouseOnPreview) this.popover.close()

View File

@ -27,6 +27,7 @@
</div>
</div>
</div>
<div class="btn-group flex-fill" role="group">
<input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="table" id="displayModeDetails" name="displayModeDetails">
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
@ -42,6 +43,13 @@
</label>
</div>
<div class="btn-group flex-fill" role="group">
<input type="checkbox" class="btn-check" [(ngModel)]="list.showPreviewPane" value="table" id="previewPane" name="previewPane">
<label for="previewPane" class="btn btn-outline-primary btn-sm">
<i-bs name="window-split"></i-bs>
</label>
</div>
<div ngbDropdown class="btn-group flex-fill">
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>
<i-bs name="arrow-down-up"></i-bs>
@ -105,298 +113,335 @@
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
</div>
<ng-template #pagination>
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
@if (list.isReloading) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
}
@if (list.selected.size > 0) {
<span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
}
@if (!list.isReloading) {
@if (list.selected.size === 0) {
<span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>
}&nbsp;@if (isFiltered) {
&nbsp;<span i18n>(filtered)</span>
<div class="row">
<div [class.col-lg-6]="list.showPreviewPane" [class.col]="!list.showPreviewPane">
<ng-template #pagination>
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
@if (list.isReloading) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
}
}
@if (!list.isReloading && isFiltered) {
<button class="btn btn-link py-0" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button>
@if (list.selected.size > 0) {
<span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
}
@if (!list.isReloading) {
@if (list.selected.size === 0) {
<span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>
}&nbsp;@if (isFiltered) {
&nbsp;<span i18n>(filtered)</span>
}
}
@if (!list.isReloading && isFiltered) {
<button class="btn btn-link py-0" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button>
}
</div>
@if (list.collectionSize) {
<ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
}
</div>
@if (list.collectionSize) {
<ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
}
</ng-template>
<div tourAnchor="tour.documents">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
</ng-template>
<div tourAnchor="tour.documents">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
@if (list.error ) {
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
} @else {
@if (list.displayMode === DisplayMode.LARGE_CARDS) {
<div>
@for (d of list.documents; track d.id) {
<pngx-document-card-large
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
[displayFields]="activeDisplayFields"
(clickTag)="clickTag($event)"
(clickCorrespondent)="clickCorrespondent($event)"
(clickDocumentType)="clickDocumentType($event)"
(clickStoragePath)="clickStoragePath($event)"
(clickMoreLike)="clickMoreLike(d.id)">
</pngx-document-card-large>
}
</div>
}
@if (list.displayMode === DisplayMode.TABLE) {
<div class="table-responsive">
<table class="table table-sm align-middle border shadow-sm">
<thead>
<tr>
<th></th>
@if (activeDisplayFields.includes(DisplayField.ASN)) {
<th class="cursor-pointer"
pngxSortable="archive_serial_number"
title="Sort by ASN" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>ASN</th>
}
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<th class="cursor-pointer"
pngxSortable="correspondent__name"
title="Sort by correspondent" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
}
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
<th class="cursor-pointer"
pngxSortable="title"
title="Sort by title" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
style="min-width: 150px;"
i18n>Title</th>
}
@if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) {
<th i18n>Tags</th>
}
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
<th class="cursor-pointer"
pngxSortable="owner"
title="Sort by owner" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Owner</th>
}
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
<th class="cursor-pointer"
pngxSortable="num_notes"
title="Sort by notes" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Notes</th>
}
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<th class="cursor-pointer"
pngxSortable="document_type__name"
title="Sort by document type" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Document type</th>
}
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<th class="cursor-pointer"
pngxSortable="storage_path__name"
title="Sort by storage path" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Storage path</th>
}
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
<th class="cursor-pointer"
pngxSortable="created"
title="Sort by created date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Created</th>
}
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
<th class="cursor-pointer"
pngxSortable="added"
title="Sort by added date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
}
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
@if (list.error ) {
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
} @else {
@if (list.displayMode === DisplayMode.LARGE_CARDS) {
<div>
@for (d of list.documents; track d.id) {
<pngx-document-card-large
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
[displayFields]="activeDisplayFields"
(clickTag)="clickTag($event)"
(clickCorrespondent)="clickCorrespondent($event)"
(clickDocumentType)="clickDocumentType($event)"
(clickStoragePath)="clickStoragePath($event)"
(clickMoreLike)="clickMoreLike(d.id)">
</pngx-document-card-large>
}
</div>
}
@if (list.displayMode === DisplayMode.TABLE) {
<div class="table-responsive">
<table class="table table-sm align-middle border shadow-sm">
<thead>
<tr>
<th></th>
@if (activeDisplayFields.includes(DisplayField.ASN)) {
<th class="cursor-pointer"
pngxSortable="page_count"
title="Sort by number of pages" i18n-title
pngxSortable="archive_serial_number"
title="Sort by ASN" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Pages</th>
}
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
<th i18n>
Shared
</th>
}
@for (field_id of activeDisplayCustomFields; track field_id) {
<th class="cursor-pointer"
pngxSortable="{{field_id}}"
title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)">
{{getDisplayCustomFieldTitle(field_id)}}
</th>
}
</tr>
</thead>
<tbody>
@for (d of list.documents; track d.id) {
<tr (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();">
<label class="form-check-label" for="docCheck{{d.id}}"></label>
</div>
</td>
@if (activeDisplayFields.includes(DisplayField.ASN)) {
<td class="">
{{d.archive_serial_number}}
</td>
i18n>ASN</th>
}
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<td class="">
@if (d.correspondent) {
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
}
</td>
<th class="cursor-pointer"
pngxSortable="correspondent__name"
title="Sort by correspondent" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
}
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
<td width="30%">
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
<div class="d-inline-block" (mouseleave)="popupPreview.close()">
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<pngx-preview-popup [document]="d" linkClasses="btn btn-sm btn-link text-secondary" linkTitle="Preview document" (click)="$event.stopPropagation()" i18n-linkTitle #popupPreview>
<i-bs name="eye"></i-bs>
</pngx-preview-popup>
</div>
}
@if (activeDisplayFields.includes(DisplayField.TAGS)) {
@for (t of d.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
}
}
</td>
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
<th class="cursor-pointer"
pngxSortable="title"
title="Sort by title" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
style="min-width: 150px;"
i18n>Title</th>
}
@if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) {
<th i18n>Tags</th>
}
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
<td>
{{d.owner | username}}
</td>
<th class="cursor-pointer"
pngxSortable="owner"
title="Sort by owner" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Owner</th>
}
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
<td class="">
@if (d.notes.length) {
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
<span class="badge rounded-pill bg-light border text-primary">
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
{{d.notes.length}}</span>
</a>
}
</td>
<th class="cursor-pointer"
pngxSortable="num_notes"
title="Sort by notes" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Notes</th>
}
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<td class="">
@if (d.document_type) {
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
}
</td>
<th class="cursor-pointer"
pngxSortable="document_type__name"
title="Sort by document type" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Document type</th>
}
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<td class="">
@if (d.storage_path) {
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
}
</td>
<th class="cursor-pointer"
pngxSortable="storage_path__name"
title="Sort by storage path" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Storage path</th>
}
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
<td>
{{d.created_date | customDate}}
</td>
<th class="cursor-pointer"
pngxSortable="created"
title="Sort by created date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Created</th>
}
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
<td>
{{d.added | customDate}}
</td>
<th class="cursor-pointer"
pngxSortable="added"
title="Sort by added date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
}
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
<td>
{{ d.page_count }}
</td>
<th class="cursor-pointer"
pngxSortable="page_count"
title="Sort by number of pages" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Pages</th>
}
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
<td>
@if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }
</td>
<th i18n>
Shared
</th>
}
@for (field of activeDisplayCustomFields; track field) {
<td class="">
<pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display>
</td>
@for (field_id of activeDisplayCustomFields; track field_id) {
<th class="cursor-pointer"
pngxSortable="{{field_id}}"
title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)">
{{getDisplayCustomFieldTitle(field_id)}}
</th>
}
</tr>
}
</tbody>
</table>
</div>
</thead>
<tbody>
@for (d of list.documents; track d.id) {
<tr (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();">
<label class="form-check-label" for="docCheck{{d.id}}"></label>
</div>
</td>
@if (activeDisplayFields.includes(DisplayField.ASN)) {
<td class="">
{{d.archive_serial_number}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<td class="">
@if (d.correspondent) {
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
<td width="30%">
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
<div class="d-inline-block" (mouseleave)="popupPreview.close()">
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<pngx-preview-popup [document]="d" linkClasses="btn btn-sm btn-link text-secondary" linkTitle="Preview document" (click)="$event.stopPropagation()" i18n-linkTitle #popupPreview>
<i-bs name="eye"></i-bs>
</pngx-preview-popup>
</div>
}
@if (activeDisplayFields.includes(DisplayField.TAGS)) {
@for (t of d.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
}
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
<td>
{{d.owner | username}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
<td class="">
@if (d.notes.length) {
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
<span class="badge rounded-pill bg-light border text-primary">
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
{{d.notes.length}}</span>
</a>
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<td class="">
@if (d.document_type) {
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<td class="">
@if (d.storage_path) {
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
<td>
{{d.created_date | customDate}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
<td>
{{d.added | customDate}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
<td>
{{ d.page_count }}
</td>
}
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
<td>
@if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }
</td>
}
@for (field of activeDisplayCustomFields; track field) {
<td class="">
<pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display>
</td>
}
</tr>
}
</tbody>
</table>
</div>
}
@if (list.displayMode === DisplayMode.SMALL_CARDS) {
<div class="row row-cols-paperless-cards">
@for (d of list.documents; track d.id) {
<pngx-document-card-small class="p-0"
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
(clickTag)="clickTag($event)"
[displayFields]="activeDisplayFields"
(clickCorrespondent)="clickCorrespondent($event)"
(clickStoragePath)="clickStoragePath($event)"
(clickDocumentType)="clickDocumentType($event)">
</pngx-document-card-small>
}
</div>
}
@if (list.documents?.length > 15) {
<div class="mt-3">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
}
}
@if (list.displayMode === DisplayMode.SMALL_CARDS) {
<div class="row row-cols-paperless-cards">
@for (d of list.documents; track d.id) {
<pngx-document-card-small class="p-0"
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
(clickTag)="clickTag($event)"
[displayFields]="activeDisplayFields"
(clickCorrespondent)="clickCorrespondent($event)"
(clickStoragePath)="clickStoragePath($event)"
(clickDocumentType)="clickDocumentType($event)">
</pngx-document-card-small>
}
</div>
@if (list.showPreviewPane) {
<div class="col-lg-6">
<div class="row">
<div class="btn-toolbar mb-1 border-bottom align-items-center">
<div class="btn-group pb-3">
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="list.documents.length === 0 || !hasPrevious">
<i-bs width="1.2em" height="1.2em" name="arrow-left" class="me-1"></i-bs><ng-container i18n>Previous</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Next" (click)="nextDoc()" [disabled]="list.documents.length === 0 || !hasNext">
<ng-container i18n>Next</ng-container><i-bs width="1.2em" height="1.2em" name="arrow-right" class="ms-1"></i-bs>
</button>
</div>
<div class="input-group pb-3 ms-auto">
<h5 class="mb-0">
{{list.firstSelectedDocument?.title}}
</h5>
</div>
</div>
</div>
}
@if (list.documents?.length > 15) {
<div class="mt-3">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
<div class="row">
<div class="col preview-pane">
@if (list.selected.size > 0) {
<pngx-preview-popup [document]="list.firstSelectedDocument" [previewOnly]="true"></pngx-preview-popup>
} @else {
<div class="w-100 h-100 position-relative">
<p class="fst-italic">
<ng-container i18n>No document selected</ng-container>
</p>
</div>
}
</div>
</div>
}
</div>
}
</div>

View File

@ -80,3 +80,9 @@ a {
pngx-page-header .dropdown-menu {
--bs-dropdown-min-width: 12em;
}
.preview-pane {
height: 60rem;
top: 70px;
position: sticky;
}

View File

@ -326,24 +326,36 @@ export class DocumentListComponent
this.hotKeyService
.addShortcut({
keys: 'control.arrowleft',
description: $localize`Previous page`,
description: $localize`Previous page / document`,
})
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.list.currentPage > 1) {
this.list.currentPage--
if (this.list.showPreviewPane) {
if (this.hasPrevious) {
this.previousDoc()
}
} else {
if (this.list.currentPage > 1) {
this.list.currentPage--
}
}
})
this.hotKeyService
.addShortcut({
keys: 'control.arrowright',
description: $localize`Next page`,
description: $localize`Next page / document`,
})
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.list.currentPage < this.list.getLastPage()) {
this.list.currentPage++
if (this.list.showPreviewPane) {
if (this.hasNext) {
this.nextDoc()
}
} else {
if (this.list.currentPage < this.list.getLastPage()) {
this.list.currentPage++
}
}
})
}
@ -473,4 +485,45 @@ export class DocumentListComponent
resetFilters() {
this.filterEditor.resetSelected()
}
public get hasPrevious(): boolean {
return (
(this.list.selected.size > 0 &&
this.list.documents.indexOf(this.list.firstSelectedDocument) > 0) ||
(this.list.selected.size === 0 && this.list.documents.length > 0)
)
}
public get hasNext(): boolean {
return (
(this.list.selected.size > 0 &&
this.list.documents.indexOf(this.list.firstSelectedDocument) <
this.list.documents.length - 1) ||
(this.list.selected.size === 0 && this.list.documents.length > 0)
)
}
public nextDoc(): void {
const index =
this.list.selected.size === 0
? 0
: Math.min(
this.list.documents.indexOf(this.list.firstSelectedDocument) + 1,
this.list.documents.length - 1
)
this.list.selected.clear()
this.list.selected.add(this.list.documents[index].id)
}
public previousDoc(): void {
const index =
this.list.selected.size === 0
? 0
: Math.max(
this.list.documents.indexOf(this.list.firstSelectedDocument) - 1,
0
)
this.list.selected.clear()
this.list.selected.add(this.list.documents[index].id)
}
}

View File

@ -79,6 +79,11 @@ export interface ListViewState {
* The fields to display in the document list.
*/
displayFields?: DisplayField[]
/**
* Whether the preview pane is shown.
*/
showPreviewPane?: boolean
}
/**
@ -165,6 +170,7 @@ export class DocumentListViewService {
sortReverse: true,
filterRules: [],
selected: new Set<number>(),
showPreviewPane: false,
}
}
@ -451,6 +457,15 @@ export class DocumentListViewService {
this.saveDocumentListView()
}
get showPreviewPane(): boolean {
return this.activeListViewState.showPreviewPane
}
set showPreviewPane(show: boolean) {
this.activeListViewState.showPreviewPane = show
this.saveDocumentListView()
}
private saveDocumentListView() {
if (this._activeSavedViewId == null) {
let savedState: ListViewState = {
@ -461,6 +476,7 @@ export class DocumentListViewService {
sortReverse: this.activeListViewState.sortReverse,
displayMode: this.activeListViewState.displayMode,
displayFields: this.activeListViewState.displayFields,
showPreviewPane: this.activeListViewState.showPreviewPane,
}
localStorage.setItem(
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
@ -626,4 +642,8 @@ export class DocumentListViewService {
documentIndexInCurrentView(documentID: number): number {
return this.documents.map((d) => d.id).indexOf(documentID)
}
get firstSelectedDocument(): Document {
return this.documents.find((d) => this.selected.has(d.id))
}
}

View File

@ -125,6 +125,7 @@ import {
trash,
uiRadios,
upcScan,
windowSplit,
windowStack,
x,
xCircle,
@ -323,6 +324,7 @@ const icons = {
trash,
uiRadios,
upcScan,
windowSplit,
windowStack,
x,
xCircle,