Enhancement: improved loading visuals (#8435)

This commit is contained in:
shamoon
2024-12-05 20:26:28 -08:00
committed by GitHub
parent 8722ff481c
commit 0647812699
35 changed files with 792 additions and 490 deletions

View File

@@ -9,8 +9,9 @@
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
}
@if (documents.length && displayMode === DisplayMode.TABLE) {
<table content class="table table-hover mb-0 mt-n2 align-middle">
<div content class="wrapper" [class.reveal]="reveal">
@if (displayMode === DisplayMode.TABLE) {
<table class="table table-hover mb-0 mt-n2 align-middle">
<thead>
<tr>
@for (field of displayFields; track field; let i = $index) {
@@ -28,53 +29,59 @@
</tr>
</thead>
<tbody>
@for (doc of documents; track doc.id) {
@for (doc of documents; track doc.id; let i = $index) {
<tr>
@for (field of displayFields; track field; let i = $index) {
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': i > 1 }">
@switch (field) {
@case (DisplayField.ADDED) {
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a>
}
@case (DisplayField.CREATED) {
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created_date | customDate}}</a>
}
@case (DisplayField.TITLE) {
<a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
}
@case (DisplayField.CORRESPONDENT) {
@if (doc.correspondent) {
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickCorrespondent(doc.correspondent, $event)" title="Filter by correspondent" i18n-title>{{(doc.correspondent$ | async)?.name}}</a>
}
}
@case (DisplayField.TAGS) {
@for (t of doc.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t.id, $event)" [clickable]="true" linkTitle="Filter by tag" i18n-title></pngx-tag>
}
}
@case (DisplayField.DOCUMENT_TYPE) {
@if (doc.document_type) {
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickDocType(doc.document_type, $event)" title="Filter by document type" i18n-title>{{(doc.document_type$ | async)?.name}}</a>
}
}
@case (DisplayField.STORAGE_PATH) {
@if (doc.storage_path) {
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickStoragePath(doc.storage_path, $event)" title="Filter by storage path" i18n-title>{{(doc.storage_path$ | async)?.name}}</a>
}
}
}
@if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
<pngx-custom-field-display [document]="doc" [fieldDisplayKey]="field"></pngx-custom-field-display>
}
@if (i === displayFields.length - 1) {
<div class="btn-group position-absolute top-50 end-0 translate-middle-y" (mouseleave)="popupPreview.close()">
<pngx-preview-popup [document]="doc" linkClasses="btn px-4 btn-dark border-dark-subtle" #popupPreview>
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
</pngx-preview-popup>
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
<i-bs width="0.8em" height="0.8em" name="download"></i-bs>
</a>
@for (field of displayFields; track field; let j = $index) {
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': j > 1 }">
@if (loading && reveal) {
<div class="placeholder-glow text-start">
<span class="placeholder bg-secondary w-50" [ngStyle]="{ opacity: 1 - (i * 1/documents.length) }"></span>
</div>
} @else {
@switch (field) {
@case (DisplayField.ADDED) {
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.added | customDate}}</a>
}
@case (DisplayField.CREATED) {
<a routerLink="/documents/{{doc.id}}" class="btn-link text-dark text-decoration-none py-2 py-md-3" title="Open document" i18n-title>{{doc.created_date | customDate}}</a>
}
@case (DisplayField.TITLE) {
<a routerLink="/documents/{{doc.id}}" title="Open document" i18n-title class="btn-link text-dark text-decoration-none py-2 py-md-3">{{doc.title | documentTitle}}</a>
}
@case (DisplayField.CORRESPONDENT) {
@if (doc.correspondent) {
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickCorrespondent(doc.correspondent, $event)" title="Filter by correspondent" i18n-title>{{(doc.correspondent$ | async)?.name}}</a>
}
}
@case (DisplayField.TAGS) {
@for (t of doc.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" (click)="clickTag(t.id, $event)" [clickable]="true" linkTitle="Filter by tag" i18n-title></pngx-tag>
}
}
@case (DisplayField.DOCUMENT_TYPE) {
@if (doc.document_type) {
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickDocType(doc.document_type, $event)" title="Filter by document type" i18n-title>{{(doc.document_type$ | async)?.name}}</a>
}
}
@case (DisplayField.STORAGE_PATH) {
@if (doc.storage_path) {
<a class="btn-link text-dark text-decoration-none" type="button" (click)="clickStoragePath(doc.storage_path, $event)" title="Filter by storage path" i18n-title>{{(doc.storage_path$ | async)?.name}}</a>
}
}
}
@if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
<pngx-custom-field-display [document]="doc" [fieldDisplayKey]="field"></pngx-custom-field-display>
}
@if (j === displayFields.length - 1) {
<div class="btn-group position-absolute top-50 end-0 translate-middle-y" (mouseleave)="popupPreview.close()">
<pngx-preview-popup [document]="doc" linkClasses="btn px-4 btn-dark border-dark-subtle" #popupPreview>
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
</pngx-preview-popup>
<a [href]="getDownloadUrl(doc)" class="btn px-4 btn-dark border-dark-subtle" title="Download" i18n-title (click)="$event.stopPropagation()">
<i-bs width="0.8em" height="0.8em" name="download"></i-bs>
</a>
</div>
}
}
</td>
}
@@ -82,13 +89,14 @@
}
</tbody>
</table>
} @else if (documents.length && displayMode === DisplayMode.SMALL_CARDS) {
<div content class="row row-cols-paperless-cards my-n2">
@for (d of documents; track d.id) {
} @else if (displayMode === DisplayMode.SMALL_CARDS) {
<div class="row row-cols-paperless-cards my-n2">
@for (d of documents; track d.id; let i = $index) {
<pngx-document-card-small
class="p-0"
[ngStyle]="{ opacity: !loading && reveal ? 1 : 1 - (i * 1/documents.length) }"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
[document]="!loading && reveal ? d : null"
[displayFields]="displayFields"
(clickTag)="clickTag($event)"
(clickCorrespondent)="clickCorrespondent($event)"
@@ -97,12 +105,13 @@
</pngx-document-card-small>
}
</div>
} @else if (documents.length && displayMode === DisplayMode.LARGE_CARDS) {
<div content class="row my-n2">
@for (d of documents; track d.id) {
} @else if (displayMode === DisplayMode.LARGE_CARDS) {
<div class="row my-n2">
@for (d of documents; track d.id; let i = $index) {
<pngx-document-card-large
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
[document]="!loading && reveal ? d : null"
[ngStyle]="{ opacity: !loading && reveal ? 1 : 1 - (i * 1/documents.length) }"
[displayFields]="displayFields"
(clickTag)="clickTag($event)"
(clickCorrespondent)="clickCorrespondent($event)"
@@ -113,8 +122,8 @@
}
</div>
} @else {
<p content i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
<p i18n class="text-center text-muted mb-0 fst-italic">No documents</p>
}
</div>
</pngx-widget-frame>

View File

@@ -1,3 +1,17 @@
.wrapper {
transition: all .3s ease-out;
overflow: hidden;
max-height: 0;
opacity: .1;
width: 100%;
}
.reveal {
max-height: 1000px;
opacity: 1;
overflow: visible;
}
table {
overflow-wrap: anywhere;
table-layout: fixed;

View File

@@ -187,7 +187,7 @@ describe('SavedViewWidgetComponent', () => {
fixture.detectChanges()
})
it('should show a list of documents', () => {
it('should show a list of documents', fakeAsync(() => {
jest.spyOn(documentService, 'listFiltered').mockReturnValue(
of({
all: [2, 3],
@@ -196,6 +196,7 @@ describe('SavedViewWidgetComponent', () => {
})
)
component.ngOnInit()
tick(500)
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).toContain('doc2')
expect(fixture.debugElement.nativeElement.textContent).toContain('doc3')
@@ -206,7 +207,7 @@ describe('SavedViewWidgetComponent', () => {
expect(
fixture.debugElement.queryAll(By.css('td a.btn'))[1].attributes['href']
).toEqual(component.getDownloadUrl(documentResults[0]))
})
}))
it('should call api endpoint and load results', () => {
const listAllSpy = jest.spyOn(documentService, 'listFiltered')

View File

@@ -7,7 +7,7 @@ import {
ViewChildren,
} from '@angular/core'
import { Router } from '@angular/router'
import { Subject, takeUntil } from 'rxjs'
import { delay, Subject, takeUntil, tap } from 'rxjs'
import {
DEFAULT_DASHBOARD_DISPLAY_FIELDS,
DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
@@ -52,7 +52,8 @@ export class SavedViewWidgetComponent
public DisplayField = DisplayField
public CustomFieldDataType = CustomFieldDataType
loading: boolean = true
public loading: boolean = true
public reveal: boolean = false
private customFields: CustomField[] = []
@@ -133,16 +134,22 @@ export class SavedViewWidgetComponent
this.documentService
.listFiltered(
1,
this.savedView.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
this.savedView?.page_size ?? DEFAULT_DASHBOARD_VIEW_PAGE_SIZE,
this.savedView.sort_field,
this.savedView.sort_reverse,
this.savedView.filter_rules,
{ truncate_content: true }
)
.pipe(takeUntil(this.unsubscribeNotifier))
.pipe(
takeUntil(this.unsubscribeNotifier),
tap((result) => {
this.reveal = true
this.documents = result.results
}),
delay(500)
)
.subscribe((result) => {
this.loading = false
this.documents = result.results
})
}