Updated dashboard

This commit is contained in:
shamoon 2023-09-23 00:10:51 -07:00
parent 96176589ca
commit 787209d1e4
15 changed files with 177 additions and 41 deletions

View File

@ -1,7 +1,9 @@
<div class="row pt-3 pb-3 pb-md-1 mb-3 border-bottom align-items-center">
<div class="col-md text-truncate">
<p class="h2 text-truncate" style="line-height: 1.4">{{title}}</p>
<p *ngIf="subTitle" class="h5 text-truncate" style="line-height: 1.4">{{subTitle}}</p>
<h3 class="text-truncate" style="line-height: 1.4">
{{title}}
<span *ngIf="subTitle" class="h6 d-block d-md-inline fw-normal ms-md-3 text-truncate" style="line-height: 1.4">{{subTitle}}</span>
</h3>
</div>
<div class="btn-toolbar col col-md-auto">
<ng-content></ng-content>

View File

@ -25,7 +25,7 @@ describe('PageHeaderComponent', () => {
component.title = 'Foo'
component.subTitle = 'Bar'
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('FooBar')
expect(fixture.nativeElement.textContent).toContain('Foo Bar')
})
it('should set html title', () => {

View File

@ -1,29 +1,36 @@
<pngx-page-header title="Dashboard" [subTitle]="subtitle" i18n-title>
<pngx-logo extra_classes="d-none d-md-block"></pngx-logo>
<pngx-logo extra_classes="d-none d-md-block mt-n2 p-1" height="4.5rem"></pngx-logo>
</pngx-page-header>
<div class="row">
<div class="col-lg-8">
<div tourAnchor="tour.dashboard">
<ng-container *ngIf="savedViewService.loading">
<div class="col col-md-9 mb-3">
<div class="row row-cols-1 g-4" tourAnchor="tour.dashboard">
<div *ngIf="savedViewService.loading" class="col">
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
</div>
<div *ngIf="settingsService.offerTour()" class="col">
<pngx-welcome-widget (dismiss)="completeTour()"></pngx-welcome-widget>
</div>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
<div class="col">
<pngx-saved-view-widget [savedView]="v"></pngx-saved-view-widget>
</div>
</ng-container>
</ng-container>
<pngx-welcome-widget *ngIf="settingsService.offerTour()" (dismiss)="completeTour()"></pngx-welcome-widget>
<div *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
<ng-container *ngFor="let v of savedViewService.dashboardViews; first as isFirst">
<pngx-saved-view-widget [savedView]="v"></pngx-saved-view-widget>
</ng-container>
</div>
</div>
<div class="col">
<div class="row row-cols-1 g-4">
<div class="col">
<pngx-statistics-widget></pngx-statistics-widget>
</div>
<div class="col">
<pngx-upload-file-widget></pngx-upload-file-widget>
</div>
</div>
</div>
<div class="col-lg-4">
<pngx-statistics-widget></pngx-statistics-widget>
<pngx-upload-file-widget></pngx-upload-file-widget>
</div>
</div>

View File

@ -3,20 +3,28 @@
<a class="btn-link" header-buttons [routerLink]="[]" (click)="showAll()" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" i18n>Show all</a>
<table content class="table table-sm table-hover table-borderless mb-0">
<table content class="table table-hover mb-0 align-middle">
<thead>
<tr>
<th scope="col" i18n>Created</th>
<th scope="col" i18n>Title</th>
<th scope="col" class="d-none d-md-table-cell" i18n>Tags</th>
<th scope="col" class="d-none d-md-table-cell" i18n>Correspondent</th>
</tr>
</thead>
<tbody *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
<tr *ngFor="let doc of documents" (mouseleave)="mouseLeaveCard()">
<td><a routerLink="/documents/{{doc.id}}" class="d-block text-dark text-decoration-none">{{doc.created_date | customDate}}</a></td>
<td class="position-relative">
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="d-block text-dark text-decoration-none">{{doc.title | documentTitle}}<pngx-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag></a>
<td class="py-3"><a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none">{{doc.created_date | customDate}}</a></td>
<td class="py-3">
<a routerLink="/documents/{{doc.id}}" title="Edit" i18n-title class="btn-link text-dark text-decoration-none">{{doc.title | documentTitle}}</a>
</td>
<td class="py-3 d-none d-md-table-cell">
<pngx-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ms-1" (click)="clickTag(t, $event)"></pngx-tag>
</td>
<td class="position-relative py-3 d-none d-md-table-cell">
<a *ngIf="doc.correspondent !== null" class="btn-link" routerLink="/documents" [queryParams]="getCorrespondentQueryParams(doc.correspondent)">{{(doc.correspondent$ | async)?.name}}</a>
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn btn-sm px-4 py-0 btn-dark border-dark-subtle"
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"
[ngbPopover]="previewContent" [popoverTitle]="doc.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" container="body" (mouseenter)="mouseEnterPreview(doc)" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<svg class="buttonicon-xs" fill="currentColor">
@ -26,7 +34,7 @@
<ng-template #previewContent>
<object [data]="getPreviewUrl(doc) | safeUrl" class="preview" width="100%"></object>
</ng-template>
<a [href]="getDownloadUrl(doc)" class="btn btn-sm px-4 py-0 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
<svg class="buttonicon-xs" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download"/>
</svg>

View File

@ -5,9 +5,12 @@ table {
th:first-child {
width: 25%;
@media (min-width: 768px) {
width: 15%;
}
}
tbody app-tag {
tbody pngx-tag {
cursor: pointer;
}
@ -22,3 +25,8 @@ tr:hover .btn-group {
opacity: 1;
pointer-events: all;
}
td.py-3 {
padding-top: 0.75em !important;
padding-bottom: 0.75em !important;
}

View File

@ -52,6 +52,7 @@ const documentResults = [
{
id: 3,
title: 'doc3',
correspondent: 0,
},
]

View File

@ -6,18 +6,22 @@ import {
QueryList,
ViewChildren,
} from '@angular/core'
import { Router } from '@angular/router'
import { Params, Router } from '@angular/router'
import { Subject, takeUntil } from 'rxjs'
import { PaperlessDocument } from 'src/app/data/paperless-document'
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { PaperlessTag } from 'src/app/data/paperless-tag'
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
import {
FILTER_CORRESPONDENT,
FILTER_HAS_TAGS_ALL,
} from 'src/app/data/filter-rule-type'
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
@Component({
selector: 'pngx-saved-view-widget',
@ -38,7 +42,8 @@ export class SavedViewWidgetComponent
private router: Router,
private list: DocumentListViewService,
private consumerStatusService: ConsumerStatusService,
public openDocumentsService: OpenDocumentsService
public openDocumentsService: OpenDocumentsService,
public documentListViewService: DocumentListViewService
) {
super()
}
@ -141,4 +146,15 @@ export class SavedViewWidgetComponent
mouseLeaveCard() {
this.popover?.close()
}
getCorrespondentQueryParams(correspondentId: number): Params {
return correspondentId !== undefined
? queryParamsFromFilterRules([
{
rule_type: FILTER_CORRESPONDENT,
value: correspondentId.toString(),
},
])
: null
}
}

View File

@ -42,5 +42,32 @@
</div>
</div>
</div>
<div class="list-group border-light mt-3">
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }">
<a *ngIf="statistics?.tag_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/tags/">
<ng-container i18n>Tags</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.tag_count | number}}</span>
</a>
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }">
<a *ngIf="statistics?.correspondent_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/correspondents/">
<ng-container i18n>Correspondents</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.correspondent_count | number}}</span>
</a>
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }">
<a *ngIf="statistics?.document_type_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/">
<ng-container i18n>Document Types</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.document_type_count | number}}</span>
</a>
</ng-container>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }">
<a *ngIf="statistics?.storage_path_count > 0" class="list-group-item d-flex justify-content-between align-items-center" routerLink="/documenttypes/">
<ng-container i18n>Storage Paths</ng-container>:
<span class="badge bg-secondary text-light rounded-pill">{{statistics?.storage_path_count | number}}</span>
</a>
</ng-container>
</div>
</ng-container>
</pngx-widget-frame>

View File

@ -11,6 +11,7 @@ import { environment } from 'src/environments/environment'
import { RouterTestingModule } from '@angular/router/testing'
import { routes } from 'src/app/app-routing.module'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
describe('StatisticsWidgetComponent', () => {
let component: StatisticsWidgetComponent
@ -19,7 +20,11 @@ describe('StatisticsWidgetComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [StatisticsWidgetComponent, WidgetFrameComponent],
declarations: [
StatisticsWidgetComponent,
WidgetFrameComponent,
IfPermissionsDirective,
],
providers: [PermissionsGuard],
imports: [
HttpClientTestingModule,

View File

@ -6,6 +6,7 @@ import { ConsumerStatusService } from 'src/app/services/consumer-status.service'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { environment } from 'src/environments/environment'
import * as mimeTypeNames from 'mime-names'
import { ComponentWithPermissions } from 'src/app/components/with-permissions/with-permissions.component'
export interface Statistics {
documents_total?: number
@ -13,6 +14,10 @@ export interface Statistics {
inbox_tag?: number
document_file_type_counts?: DocumentFileType[]
character_count?: number
tag_count?: number
correspondent_count?: number
document_type_count?: number
storage_path_count?: number
}
interface DocumentFileType {
@ -25,14 +30,19 @@ interface DocumentFileType {
templateUrl: './statistics-widget.component.html',
styleUrls: ['./statistics-widget.component.scss'],
})
export class StatisticsWidgetComponent implements OnInit, OnDestroy {
export class StatisticsWidgetComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy
{
loading: boolean = true
constructor(
private http: HttpClient,
private consumerStatusService: ConsumerStatusService,
private documentListViewService: DocumentListViewService
) {}
) {
super()
}
statistics: Statistics = {}

View File

@ -8,12 +8,12 @@
</svg>
</a>
</div>
<div content tourAnchor="tour.upload-widget">
<form>
<div content tourAnchor="tour.upload-widget" class="h-100">
<form class="h-100">
<ngx-file-drop dropZoneLabel="Drop documents here or" browseBtnLabel="Browse files" (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
multiple="true" contentClassName="justify-content-center d-flex align-items-center py-5 px-2" [showBrowseBtn]=true
browseBtnClassName="btn btn-sm btn-outline-primary ms-2" i18n-dropZoneLabel i18n-browseBtnLabel>
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card h-100"
multiple="true" contentClassName="justify-content-center d-flex flex-column text-muted align-items-center py-5 px-2 h-100" [showBrowseBtn]=true
browseBtnClassName="btn btn-sm btn-outline-primary mt-2" i18n-dropZoneLabel i18n-browseBtnLabel>
</ngx-file-drop>
</form>
<p class="mt-3" *ngIf="getStatus().length > 0">{{getStatusSummary()}}</p>

View File

@ -1,7 +1,7 @@
<div class="card mb-3 shadow-sm bg-light">
<div class="card h-100 shadow-sm bg-light">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">{{title}}</h5>
<h6 class="card-title mb-0">{{title}}</h6>
<ng-container *ngIf="loading">
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>

View File

@ -214,6 +214,10 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
}
}
.card .table {
border-color: var(--bs-gray-800);
}
.alert-secondary {
background-color: var(--bs-light);
border-color: var(--pngx-bg-darker);

View File

@ -1571,6 +1571,13 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
)
tag_inbox = Tag.objects.create(name="t1", is_inbox_tag=True)
Tag.objects.create(name="t2")
Tag.objects.create(name="t3")
Correspondent.objects.create(name="c1")
Correspondent.objects.create(name="c2")
DocumentType.objects.create(name="dt1")
StoragePath.objects.create(name="sp1")
StoragePath.objects.create(name="sp2")
doc1.tags.add(tag_inbox)
@ -1588,6 +1595,10 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
1,
)
self.assertEqual(response.data["character_count"], 11)
self.assertEqual(response.data["tag_count"], 3)
self.assertEqual(response.data["correspondent_count"], 2)
self.assertEqual(response.data["document_type_count"], 1)
self.assertEqual(response.data["storage_path_count"], 2)
def test_statistics_no_inbox_tag(self):
Document.objects.create(title="none1", checksum="A")

View File

@ -907,6 +907,39 @@ class StatisticsView(APIView):
if user is None
else get_objects_for_user_owner_aware(user, "documents.view_tag", Tag)
)
correspondent_count = (
Correspondent.objects.count()
if user is None
else len(
get_objects_for_user_owner_aware(
user,
"documents.view_correspondent",
Correspondent,
),
)
)
document_type_count = (
DocumentType.objects.count()
if user is None
else len(
get_objects_for_user_owner_aware(
user,
"documents.view_documenttype",
DocumentType,
),
)
)
storage_path_count = (
StoragePath.objects.count()
if user is None
else len(
get_objects_for_user_owner_aware(
user,
"documents.view_storagepath",
StoragePath,
),
)
)
documents_total = documents.count()
@ -941,6 +974,10 @@ class StatisticsView(APIView):
"inbox_tag": inbox_tag.first().pk if inbox_tag.exists() else None,
"document_file_type_counts": document_file_type_counts,
"character_count": character_count,
"tag_count": len(tags),
"correspondent_count": correspondent_count,
"document_type_count": document_type_count,
"storage_path_count": storage_path_count,
},
)