Merge branch 'dev' into feature-bulk-edit-cf-values
This commit is contained in:
commit
490e48c131
File diff suppressed because it is too large
Load Diff
@ -47,7 +47,7 @@
|
||||
</tr>
|
||||
}
|
||||
@for (document of documentsInTrash; track document.id) {
|
||||
<tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()">
|
||||
<tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()" class="data-row" [class.reveal]="reveal">
|
||||
<td>
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
|
||||
|
@ -0,0 +1,8 @@
|
||||
.data-row {
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.reveal {
|
||||
opacity: 1;
|
||||
}
|
@ -69,6 +69,7 @@ describe('TrashComponent', () => {
|
||||
})
|
||||
|
||||
it('should call correct service method on reload', () => {
|
||||
jest.useFakeTimers()
|
||||
const trashSpy = jest.spyOn(trashService, 'getTrash')
|
||||
trashSpy.mockReturnValue(
|
||||
of({
|
||||
@ -78,6 +79,7 @@ describe('TrashComponent', () => {
|
||||
})
|
||||
)
|
||||
component.reload()
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(trashSpy).toHaveBeenCalled()
|
||||
expect(component.documentsInTrash).toEqual(documentsInTrash)
|
||||
})
|
||||
|
@ -4,7 +4,7 @@ import { Document } from 'src/app/data/document'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { TrashService } from 'src/app/services/trash.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { delay, Subject, takeUntil, tap } from 'rxjs'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { Router } from '@angular/router'
|
||||
@ -21,6 +21,7 @@ export class TrashComponent implements OnDestroy {
|
||||
public page: number = 1
|
||||
public totalDocuments: number
|
||||
public isLoading: boolean = false
|
||||
public reveal: boolean = false
|
||||
unsubscribeNotifier: Subject<void> = new Subject()
|
||||
|
||||
constructor(
|
||||
@ -40,12 +41,20 @@ export class TrashComponent implements OnDestroy {
|
||||
|
||||
reload() {
|
||||
this.isLoading = true
|
||||
this.trashService.getTrash(this.page).subscribe((r) => {
|
||||
this.documentsInTrash = r.results
|
||||
this.totalDocuments = r.count
|
||||
this.isLoading = false
|
||||
this.selectedDocuments.clear()
|
||||
})
|
||||
this.trashService
|
||||
.getTrash(this.page)
|
||||
.pipe(
|
||||
tap((r) => {
|
||||
this.documentsInTrash = r.results
|
||||
this.totalDocuments = r.count
|
||||
this.selectedDocuments.clear()
|
||||
}),
|
||||
delay(100)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.reveal = true
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
delete(document: Document) {
|
||||
|
@ -38,8 +38,10 @@ describe('SwitchComponent', () => {
|
||||
expect(component.value).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should show note if unset', () => {
|
||||
it('should correctly report unset', () => {
|
||||
component.value = null
|
||||
expect(component.isUnset).toBeTruthy()
|
||||
component.value = undefined
|
||||
expect(component.isUnset).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
@ -18,7 +18,7 @@
|
||||
<div class="ms-n2 me-1">
|
||||
<i-bs name="grip-vertical"></i-bs>
|
||||
</div>
|
||||
<h6 class="card-title mb-0" i18n>Loading...</h6>
|
||||
<h6 class="card-title mb-0"></h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,8 +9,9 @@
|
||||
<a class="btn-link text-decoration-none" header-buttons [routerLink]="[]" (click)="showAll()" i18n>Show all</a>
|
||||
}
|
||||
|
||||
<div content class="wrapper" [class.reveal]="reveal">
|
||||
@if (displayMode === DisplayMode.TABLE) {
|
||||
<table content class="table table-hover mb-0 mt-n2 align-middle">
|
||||
<table class="table table-hover mb-0 mt-n2 align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
@for (field of displayFields; track field; let i = $index) {
|
||||
@ -28,71 +29,59 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (loading) {
|
||||
@for (i of [].constructor(pageSize); track i) {
|
||||
<tr>
|
||||
@for (field of displayFields; track field; let j = $index) {
|
||||
<td class="py-2 py-md" [ngClass]="{ 'd-none d-md-table-cell': j > 1 }">
|
||||
<div class="placeholder-glow text-start py-2">
|
||||
<span class="placeholder bg-secondary w-50"></span>
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
@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">
|
||||
<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)="mouseEnterPreviewButton(doc)" (mouseleave)="mouseLeavePreviewButton()" #popover="ngbPopover">
|
||||
<i-bs width="0.8em" height="0.8em" name="eye"></i-bs>
|
||||
</a>
|
||||
<ng-template #previewContent>
|
||||
<pngx-preview-popup [document]="doc" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()"></pngx-preview-popup>
|
||||
</ng-template>
|
||||
<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>
|
||||
}
|
||||
@ -101,17 +90,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
} @else if (displayMode === DisplayMode.SMALL_CARDS) {
|
||||
<div content class="row row-cols-paperless-cards my-n2">
|
||||
@if (loading) {
|
||||
@for (i of [].constructor(pageSize); track i) {
|
||||
<pngx-document-card-small class="p-0"></pngx-document-card-small>
|
||||
}
|
||||
}
|
||||
@for (d of documents; track d.id) {
|
||||
<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)"
|
||||
@ -121,16 +106,12 @@
|
||||
}
|
||||
</div>
|
||||
} @else if (displayMode === DisplayMode.LARGE_CARDS) {
|
||||
<div content class="row my-n2">
|
||||
@if (loading) {
|
||||
@for (i of [].constructor(pageSize); track i) {
|
||||
<pngx-document-card-large></pngx-document-card-large>
|
||||
}
|
||||
}
|
||||
@for (d of documents; track d.id) {
|
||||
<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)"
|
||||
@ -141,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>
|
||||
|
@ -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;
|
||||
|
@ -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,43 +196,17 @@ describe('SavedViewWidgetComponent', () => {
|
||||
})
|
||||
)
|
||||
component.ngOnInit()
|
||||
tick(500)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain('doc2')
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain('doc3')
|
||||
// preview + download buttons
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.css('td a.btn'))[0].attributes['href']
|
||||
).toEqual(component.getPreviewUrl(documentResults[0]))
|
||||
).toEqual(documentService.getPreviewUrl(documentResults[0].id))
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.css('td a.btn'))[1].attributes['href']
|
||||
).toEqual(component.getDownloadUrl(documentResults[0]))
|
||||
})
|
||||
|
||||
it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
|
||||
jest.spyOn(documentService, 'listFiltered').mockReturnValue(
|
||||
of({
|
||||
all: [2, 3],
|
||||
count: 2,
|
||||
results: documentResults,
|
||||
})
|
||||
)
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
component.mouseEnterPreviewButton(documentResults[0])
|
||||
expect(component.popover.isOpen()).toBeTruthy()
|
||||
expect(component.popoverHidden).toBeTruthy()
|
||||
tick(600)
|
||||
expect(component.popoverHidden).toBeFalsy()
|
||||
component.maybeClosePopover()
|
||||
|
||||
component.mouseEnterPreviewButton(documentResults[1])
|
||||
tick(100)
|
||||
component.mouseLeavePreviewButton()
|
||||
component.mouseEnterPreview()
|
||||
expect(component.popover.isOpen()).toBeTruthy()
|
||||
component.mouseLeavePreview()
|
||||
tick(600)
|
||||
expect(component.popover.isOpen()).toBeFalsy()
|
||||
}))
|
||||
|
||||
it('should call api endpoint and load results', () => {
|
||||
|
@ -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[] = []
|
||||
|
||||
@ -137,16 +138,22 @@ export class SavedViewWidgetComponent
|
||||
this.documentService
|
||||
.listFiltered(
|
||||
1,
|
||||
this.pageSize,
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@ -206,55 +213,10 @@ export class SavedViewWidgetComponent
|
||||
this.router.navigate(['documents', document.id])
|
||||
}
|
||||
|
||||
getPreviewUrl(document: Document): string {
|
||||
return this.documentService.getPreviewUrl(document.id)
|
||||
}
|
||||
|
||||
getDownloadUrl(document: Document): string {
|
||||
return this.documentService.getDownloadUrl(document.id)
|
||||
}
|
||||
|
||||
mouseEnterPreviewButton(doc: Document) {
|
||||
const newPopover = this.popovers.get(this.documents.indexOf(doc))
|
||||
if (this.popover !== newPopover && this.popover?.isOpen())
|
||||
this.popover.close()
|
||||
this.popover = newPopover
|
||||
this.mouseOnPreview = true
|
||||
if (!this.popover.isOpen()) {
|
||||
// we're going to open but hide to pre-load content during hover delay
|
||||
this.popover.open()
|
||||
this.popoverHidden = true
|
||||
setTimeout(() => {
|
||||
if (this.mouseOnPreview) {
|
||||
// show popover
|
||||
this.popoverHidden = false
|
||||
} else {
|
||||
this.popover.close()
|
||||
}
|
||||
}, 600)
|
||||
}
|
||||
}
|
||||
|
||||
mouseEnterPreview() {
|
||||
this.mouseOnPreview = true
|
||||
}
|
||||
|
||||
mouseLeavePreview() {
|
||||
this.mouseOnPreview = false
|
||||
this.maybeClosePopover()
|
||||
}
|
||||
|
||||
mouseLeavePreviewButton() {
|
||||
this.mouseOnPreview = false
|
||||
this.maybeClosePopover()
|
||||
}
|
||||
|
||||
maybeClosePopover() {
|
||||
setTimeout(() => {
|
||||
if (!this.mouseOnPreview) this.popover?.close()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
public getColumnTitle(field: DisplayField): string {
|
||||
if (field.startsWith(DisplayField.CUSTOM_FIELD)) {
|
||||
const id = field.split('_')[2]
|
||||
|
@ -1,6 +1,6 @@
|
||||
<pngx-widget-frame title="Statistics" [loading]="loading" i18n-title>
|
||||
<ng-container content>
|
||||
<div class="list-group border-light">
|
||||
<div class="list-group border-light placeholder-glow">
|
||||
@if (loading) {
|
||||
<div class="list-group-item d-flex">
|
||||
<div class="placeholder w-50"></div>
|
||||
@ -18,7 +18,7 @@
|
||||
<div class="placeholder w-25"></div>
|
||||
<span class="placeholder badge rounded-pill ms-auto" style="width: 25px;"> </span>
|
||||
</div>
|
||||
<div class="list-group-item filetypes placeholder-glow">
|
||||
<div class="list-group-item filetypes">
|
||||
<div class="placeholder w-100 d-block mb-2"></div>
|
||||
<div class="placeholder w-100 d-block mb-2"></div>
|
||||
<div class="placeholder w-100 d-block"></div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="card shadow-sm bg-light" cdkDrag [cdkDragDisabled]="!draggable" cdkDragPreviewContainer="parent">
|
||||
<div class="card shadow-sm bg-light fade" [class.reveal]="reveal" cdkDrag [cdkDragDisabled]="!draggable" cdkDragPreviewContainer="parent">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex">
|
||||
|
@ -1,3 +1,12 @@
|
||||
i-bs {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.card {
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.reveal {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ describe('WidgetFrameComponent', () => {
|
||||
|
||||
fixture = TestBed.createComponent(WidgetFrameComponent)
|
||||
component = fixture.componentInstance
|
||||
jest.useFakeTimers()
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
@ -51,4 +52,10 @@ describe('WidgetFrameComponent', () => {
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('.spinner-border'))).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should reveal', () => {
|
||||
expect(component.reveal).toBeFalsy()
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(component.reveal).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { AfterViewInit, Component, Input } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-widget-frame',
|
||||
templateUrl: './widget-frame.component.html',
|
||||
styleUrls: ['./widget-frame.component.scss'],
|
||||
})
|
||||
export class WidgetFrameComponent {
|
||||
export class WidgetFrameComponent implements AfterViewInit {
|
||||
constructor() {}
|
||||
|
||||
@Input()
|
||||
@ -16,4 +16,12 @@ export class WidgetFrameComponent {
|
||||
|
||||
@Input()
|
||||
draggable: any
|
||||
|
||||
public reveal: boolean = false
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
setTimeout(() => {
|
||||
this.reveal = true
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="card mb-3 shadow-sm bg-light placeholder-glow" [class.card-selected]="selected" [class.document-card]="selectable" (mouseleave)="mouseLeaveCard()">
|
||||
<div class="card document-card-large mb-3 shadow-sm bg-light placeholder-glow" [class.card-selected]="selected" [class.document-card]="selectable" (mouseleave)="mouseLeaveCard()">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-2 doc-img-container rounded-start" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()">
|
||||
@if (document) {
|
||||
|
@ -120,4 +120,12 @@ describe('DocumentCardLargeComponent', () => {
|
||||
expect(fixture.nativeElement.textContent).toContain('bananas')
|
||||
expect(component.searchNoteHighlights).toContain('<span>bananas</span>')
|
||||
})
|
||||
|
||||
it('should try to close the preview on mouse leave', () => {
|
||||
component.popupPreview = {
|
||||
close: jest.fn(),
|
||||
} as any
|
||||
component.mouseLeaveCard()
|
||||
expect(component.popupPreview.close).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
@ -108,12 +108,8 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||
return this.documentService.getDownloadUrl(this.document.id)
|
||||
}
|
||||
|
||||
get previewUrl() {
|
||||
return this.documentService.getPreviewUrl(this.document.id)
|
||||
}
|
||||
|
||||
mouseLeaveCard() {
|
||||
this.popupPreview.close()
|
||||
this.popupPreview?.close()
|
||||
}
|
||||
|
||||
get contentTrimmed() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="col p-2 h-100 placeholder-glow">
|
||||
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" (mouseleave)="mouseLeaveCard()">
|
||||
<div class="col p-2 h-100">
|
||||
<div class="card h-100 shadow-sm document-card" [class.placeholder-glow]="!document" [class.card-selected]="selected" (mouseleave)="mouseLeaveCard()">
|
||||
<div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
|
||||
@if (document) {
|
||||
<img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
|
||||
@ -14,7 +14,7 @@
|
||||
<div class="placeholder bg-secondary w-100 card-img doc-img"></div>
|
||||
}
|
||||
|
||||
@if (displayFields?.includes(DisplayField.TAGS) && document) {
|
||||
@if (document && displayFields?.includes(DisplayField.TAGS)) {
|
||||
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6">
|
||||
@for (t of getTagsLimited$() | async; track t) {
|
||||
<pngx-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
|
||||
@ -38,12 +38,17 @@
|
||||
|
||||
<div class="card-body bg-light p-2">
|
||||
<p class="card-text">
|
||||
@if (document && displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>
|
||||
@if (displayFields.includes(DisplayField.TITLE)) {:}
|
||||
}
|
||||
@if (document && displayFields.includes(DisplayField.TITLE)) {
|
||||
{{document.title | documentTitle}}
|
||||
@if (document) {
|
||||
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
|
||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>
|
||||
@if (displayFields.includes(DisplayField.TITLE)) {:}
|
||||
}
|
||||
@if (displayFields.includes(DisplayField.TITLE)) {
|
||||
{{document.title | documentTitle}}
|
||||
}
|
||||
} @else {
|
||||
<div class="placeholder bg-secondary w-100"></div>
|
||||
<div class="placeholder bg-secondary w-50"></div>
|
||||
}
|
||||
@if (!document) {
|
||||
<div class="placeholder bg-secondary w-100"></div>
|
||||
|
@ -111,4 +111,12 @@ describe('DocumentCardSmallComponent', () => {
|
||||
fixture.debugElement.queryAll(By.directive(TagComponent))
|
||||
).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('should try to close the preview on mouse leave', () => {
|
||||
component.popupPreview = {
|
||||
close: jest.fn(),
|
||||
} as any
|
||||
component.mouseLeaveCard()
|
||||
expect(component.popupPreview.close).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
@ -94,7 +94,7 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
|
||||
}
|
||||
|
||||
mouseLeaveCard() {
|
||||
this.popupPreview.close()
|
||||
this.popupPreview?.close()
|
||||
}
|
||||
|
||||
get notesEnabled(): boolean {
|
||||
|
@ -12,7 +12,6 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||
import { takeUntil } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-correspondent-list',
|
||||
@ -65,24 +64,7 @@ export class CorrespondentListComponent extends ManagementListComponent<Correspo
|
||||
}
|
||||
|
||||
public reloadData(): void {
|
||||
this.isLoading = true
|
||||
this.clearSelection()
|
||||
this.service
|
||||
.listFiltered(
|
||||
this.page,
|
||||
null,
|
||||
this.sortField,
|
||||
this.sortReverse,
|
||||
this._nameFilter,
|
||||
true,
|
||||
{ last_correspondence: true }
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((c) => {
|
||||
this.data = c.results
|
||||
this.collectionSize = c.count
|
||||
this.isLoading = false
|
||||
})
|
||||
super.reloadData({ last_correspondence: true })
|
||||
}
|
||||
|
||||
getDeleteMessage(object: Correspondent) {
|
||||
|
@ -13,16 +13,23 @@
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="row reveal">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Data Type</div>
|
||||
<div class="col" i18n>Actions</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (loading) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (field of fields; track field) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="row" [class.reveal]="reveal">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{getDataType(field)}}</div>
|
||||
<div class="col">
|
||||
@ -59,7 +66,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (fields.length === 0) {
|
||||
@if (!loading && fields.length === 0) {
|
||||
<li class="list-group-item" i18n>No fields defined.</li>
|
||||
}
|
||||
</ul>
|
||||
|
@ -2,3 +2,12 @@
|
||||
.d-block.d-sm-none .dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-group-item .row {
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.list-group-item .reveal {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -95,6 +95,8 @@ describe('CustomFieldsComponent', () => {
|
||||
fixture = TestBed.createComponent(CustomFieldsComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
jest.useFakeTimers()
|
||||
jest.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
it('should support create, show notification on error / success', () => {
|
||||
@ -119,6 +121,7 @@ describe('CustomFieldsComponent', () => {
|
||||
editDialog.succeeded.emit(fields[0])
|
||||
expect(toastInfoSpy).toHaveBeenCalled()
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
jest.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
it('should support edit, show notification on error / success', () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { delay, Subject, takeUntil, tap } from 'rxjs'
|
||||
import { DATA_TYPE_LABELS, CustomField } from 'src/app/data/custom-field'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
@ -28,6 +28,9 @@ export class CustomFieldsComponent
|
||||
{
|
||||
public fields: CustomField[] = []
|
||||
|
||||
public loading: boolean = true
|
||||
public reveal: boolean = false
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
constructor(
|
||||
private customFieldsService: CustomFieldsService,
|
||||
@ -47,9 +50,16 @@ export class CustomFieldsComponent
|
||||
reload() {
|
||||
this.customFieldsService
|
||||
.listAll()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((r) => {
|
||||
this.fields = r.results
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((r) => {
|
||||
this.fields = r.results
|
||||
}),
|
||||
delay(100)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.reveal = true
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
</h4>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="row reveal">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col" i18n>Server</div>
|
||||
<div class="col d-none d-sm-block" i18n>Username</div>
|
||||
@ -34,9 +34,16 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (loadingAccounts) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (account of mailAccounts; track account) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="row" [class.reveal]="revealAccounts">
|
||||
<div class="col d-flex align-items-center">
|
||||
<button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">
|
||||
{{account.name}}@switch (account.account_type) {
|
||||
@ -76,7 +83,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (mailAccounts.length === 0) {
|
||||
@if (!loadingAccounts && mailAccounts.length === 0) {
|
||||
<li class="list-group-item" i18n>No mail accounts defined.</li>
|
||||
}
|
||||
</ul>
|
||||
@ -92,7 +99,7 @@
|
||||
</h4>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="row reveal">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col d-none d-sm-block" i18n>Sort Order</div>
|
||||
<div class="col" i18n>Account</div>
|
||||
@ -101,9 +108,16 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (loadingRules) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (rule of mailRules; track rule) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="row" [class.reveal]="revealRules">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
|
||||
<div class="col d-flex align-items-center d-none d-sm-flex">{{rule.order}}</div>
|
||||
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
|
||||
@ -151,7 +165,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (mailRules.length === 0) {
|
||||
@if (!loadingRules && mailRules.length === 0) {
|
||||
<li class="list-group-item" i18n>No mail rules defined.</li>
|
||||
}
|
||||
</ul>
|
||||
|
@ -2,3 +2,12 @@
|
||||
.d-block.d-sm-none .dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-group-item .row {
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.list-group-item .reveal {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -129,6 +129,8 @@ describe('MailComponent', () => {
|
||||
fixture = TestBed.createComponent(MailComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
jest.useFakeTimers()
|
||||
jest.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
function completeSetup(excludeService = null) {
|
||||
@ -386,6 +388,7 @@ describe('MailComponent', () => {
|
||||
component.oAuthAccountId = 3
|
||||
const editSpy = jest.spyOn(component, 'editMailAccount')
|
||||
component.ngOnInit()
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, first, takeUntil } from 'rxjs'
|
||||
import { Subject, delay, first, takeUntil, tap } from 'rxjs'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
import { MailAccount, MailAccountType } from 'src/app/data/mail-account'
|
||||
import { MailRule } from 'src/app/data/mail-rule'
|
||||
@ -47,6 +47,11 @@ export class MailComponent
|
||||
return this.settingsService.get(SETTINGS_KEYS.OUTLOOK_OAUTH_URL)
|
||||
}
|
||||
|
||||
public loadingRules: boolean = true
|
||||
public revealRules: boolean = false
|
||||
public loadingAccounts: boolean = true
|
||||
public revealAccounts: boolean = false
|
||||
|
||||
constructor(
|
||||
public mailAccountService: MailAccountService,
|
||||
public mailRuleService: MailRuleService,
|
||||
@ -62,9 +67,10 @@ export class MailComponent
|
||||
ngOnInit(): void {
|
||||
this.mailAccountService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (r) => {
|
||||
.pipe(
|
||||
first(),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((r) => {
|
||||
this.mailAccounts = r.results
|
||||
if (this.oAuthAccountId) {
|
||||
this.editMailAccount(
|
||||
@ -73,6 +79,13 @@ export class MailComponent
|
||||
)
|
||||
)
|
||||
}
|
||||
}),
|
||||
delay(100)
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.loadingAccounts = false
|
||||
this.revealAccounts = true
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError(
|
||||
@ -84,10 +97,18 @@ export class MailComponent
|
||||
|
||||
this.mailRuleService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.pipe(
|
||||
first(),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((r) => {
|
||||
this.mailRules = r.results
|
||||
}),
|
||||
delay(100)
|
||||
)
|
||||
.subscribe({
|
||||
next: (r) => {
|
||||
this.mailRules = r.results
|
||||
this.loadingRules = false
|
||||
this.revealRules = true
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error retrieving mail rules`, e)
|
||||
|
@ -53,7 +53,7 @@
|
||||
</tr>
|
||||
}
|
||||
@for (object of data; track object) {
|
||||
<tr (click)="toggleSelected(object); $event.stopPropagation();">
|
||||
<tr (click)="toggleSelected(object); $event.stopPropagation();" class="data-row" [class.reveal]="reveal">
|
||||
<td>
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
|
||||
|
@ -10,3 +10,12 @@ tbody tr:last-child td {
|
||||
.form-check {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.reveal {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -150,6 +150,7 @@ describe('ManagementListComponent', () => {
|
||||
fixture.detectChanges()
|
||||
expect(component.nameFilter).toBeNull()
|
||||
expect(component.data).toEqual(tags)
|
||||
tick(100) // load
|
||||
}))
|
||||
|
||||
it('should support create, show notification on error / success', () => {
|
||||
|
@ -7,7 +7,13 @@ import {
|
||||
} from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject } from 'rxjs'
|
||||
import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'
|
||||
import {
|
||||
debounceTime,
|
||||
delay,
|
||||
distinctUntilChanged,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import {
|
||||
MatchingModel,
|
||||
MATCHING_ALGORITHMS,
|
||||
@ -89,6 +95,8 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
public selectedObjects: Set<number> = new Set()
|
||||
public togggleAll: boolean = false
|
||||
|
||||
public reveal: boolean = false
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reloadData()
|
||||
|
||||
@ -132,7 +140,7 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
this.reloadData()
|
||||
}
|
||||
|
||||
reloadData() {
|
||||
reloadData(extraParams: { [key: string]: any } = null) {
|
||||
this.isLoading = true
|
||||
this.clearSelection()
|
||||
this.service
|
||||
@ -142,12 +150,19 @@ export abstract class ManagementListComponent<T extends ObjectWithId>
|
||||
this.sortField,
|
||||
this.sortReverse,
|
||||
this._nameFilter,
|
||||
true
|
||||
true,
|
||||
extraParams
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((c) => {
|
||||
this.data = c.results
|
||||
this.collectionSize = c.count
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((c) => {
|
||||
this.data = c.results
|
||||
this.collectionSize = c.count
|
||||
}),
|
||||
delay(100)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.reveal = true
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
<ul class="list-group">
|
||||
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="row reveal">
|
||||
<div class="col" i18n>Name</div>
|
||||
<div class="col d-none d-sm-flex" i18n>Sort order</div>
|
||||
<div class="col" i18n>Status</div>
|
||||
@ -22,9 +22,16 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (loading) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (workflow of workflows; track workflow.id) {
|
||||
<li class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="row" [class.reveal]="reveal">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editWorkflow(workflow)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Workflow)">{{workflow.name}}</button></div>
|
||||
<div class="col d-flex align-items-center d-none d-sm-flex"><code>{{workflow.order}}</code></div>
|
||||
<div class="col d-flex align-items-center">
|
||||
@ -69,7 +76,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (workflows.length === 0) {
|
||||
<li class="list-group-item" i18n>No workflows defined.</li>
|
||||
@if (!loading && workflows.length === 0) {
|
||||
<li class="list-group-item" [class.reveal]="reveal" i18n>No workflows defined.</li>
|
||||
}
|
||||
</ul>
|
||||
|
@ -2,3 +2,12 @@
|
||||
.d-block.d-sm-none .dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-group-item .row {
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
.list-group-item .reveal {
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -119,10 +119,11 @@ describe('WorkflowsComponent', () => {
|
||||
)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
|
||||
jest.useFakeTimers()
|
||||
fixture = TestBed.createComponent(WorkflowsComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
jest.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
it('should support create, show notification on error / success', () => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { WorkflowService } from 'src/app/services/rest/workflow.service'
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { Subject, takeUntil } from 'rxjs'
|
||||
import { delay, Subject, takeUntil, tap } from 'rxjs'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
@ -26,6 +26,9 @@ export class WorkflowsComponent
|
||||
|
||||
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
public loading: boolean = false
|
||||
public reveal: boolean = false
|
||||
|
||||
constructor(
|
||||
private workflowService: WorkflowService,
|
||||
public permissionsService: PermissionsService,
|
||||
@ -40,11 +43,17 @@ export class WorkflowsComponent
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.loading = true
|
||||
this.workflowService
|
||||
.listAll()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((r) => {
|
||||
this.workflows = r.results
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((r) => (this.workflows = r.results)),
|
||||
delay(100)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.reveal = true
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {
|
||||
BRIGHTNESS,
|
||||
componentToHex,
|
||||
computeLuminance,
|
||||
estimateBrightnessForColor,
|
||||
hexToHsl,
|
||||
hslToRgb,
|
||||
randomColor,
|
||||
rgbToHsl,
|
||||
} from './color'
|
||||
@ -44,7 +46,21 @@ describe('Color Utils', () => {
|
||||
expect(hsl).toEqual([0, 0, 0.5019607843137255])
|
||||
})
|
||||
|
||||
it('should convert hsl to rgb', () => {
|
||||
let rgb = hslToRgb(0, 0, 0.5)
|
||||
expect(rgb).toEqual([127.5, 127.5, 127.5])
|
||||
expect(hslToRgb(0, 0, 0)).toEqual([0, 0, 0])
|
||||
expect(hslToRgb(0, 0, 1)).toEqual([255, 255, 255])
|
||||
})
|
||||
|
||||
it('should return a random color', () => {
|
||||
expect(randomColor()).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should convert component to hex', () => {
|
||||
expect(componentToHex(0)).toEqual('00')
|
||||
expect(componentToHex(255)).toEqual('ff')
|
||||
expect(componentToHex(128)).toEqual('80')
|
||||
expect(componentToHex(15)).toEqual('0f')
|
||||
})
|
||||
})
|
||||
|
@ -5,7 +5,7 @@ export const BRIGHTNESS = {
|
||||
DARK: 'dark',
|
||||
}
|
||||
|
||||
function componentToHex(c) {
|
||||
export function componentToHex(c) {
|
||||
var hex = Math.floor(c).toString(16)
|
||||
return hex.length == 1 ? '0' + hex : hex
|
||||
}
|
||||
@ -23,21 +23,22 @@ function componentToHex(c) {
|
||||
* @param Number l The lightness
|
||||
* @return Array The RGB representation
|
||||
*/
|
||||
function hslToRgb(h, s, l) {
|
||||
|
||||
function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1
|
||||
if (t > 1) t -= 1
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t
|
||||
if (t < 1 / 2) return q
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
|
||||
return p
|
||||
}
|
||||
|
||||
export function hslToRgb(h, s, l) {
|
||||
var r, g, b
|
||||
|
||||
if (s == 0) {
|
||||
r = g = b = l // achromatic
|
||||
} else {
|
||||
function hue2rgb(p, q, t) {
|
||||
if (t < 0) t += 1
|
||||
if (t > 1) t -= 1
|
||||
if (t < 1 / 6) return p + (q - p) * 6 * t
|
||||
if (t < 1 / 2) return q
|
||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
|
||||
return p
|
||||
}
|
||||
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s
|
||||
var p = 2 * l - q
|
||||
r = hue2rgb(p, q, h + 1 / 3)
|
||||
|
@ -203,19 +203,23 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
||||
|
||||
@supports (hanging-punctuation: first) and (font: -apple-system-body) and (-webkit-appearance: none) {
|
||||
// Safari does not like the filters on the image, see https://github.com/paperless-ngx/paperless-ngx/pull/8121
|
||||
.doc-img-container {
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
.document-card:not(.placeholder-glow),
|
||||
.document-card-large:not(.placeholder-glow) {
|
||||
.doc-img-container {
|
||||
background-color: #ffffff !important;
|
||||
width: 101%;
|
||||
}
|
||||
|
||||
.doc-img {
|
||||
filter: none !important;
|
||||
box-shadow: inset 0px 0px 0px 10px rgba(0,0,0,1);
|
||||
}
|
||||
.doc-img {
|
||||
filter: none !important;
|
||||
box-shadow: inset 0px 0px 0px 10px rgba(0,0,0,1);
|
||||
}
|
||||
|
||||
.doc-img.inverted {
|
||||
filter: none !important;
|
||||
mix-blend-mode: difference;
|
||||
opacity: 0.95;
|
||||
.doc-img.inverted {
|
||||
filter: none !important;
|
||||
mix-blend-mode: difference;
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user