mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-11-13 01:56:41 -05:00
Manage dialog polling
This commit is contained in:
parent
01e9988bd2
commit
92be4e37ab
@ -16,6 +16,11 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (!loading && !error) {
|
@if (!loading && !error) {
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<p class="mb-0 text-muted small">
|
||||||
|
<ng-container i18n>Status updates every few seconds while bundles are being prepared.</ng-container>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@if (bundles.length === 0) {
|
@if (bundles.length === 0) {
|
||||||
<p class="mb-0 text-muted fst-italic" i18n>No bulk share links currently exist.</p>
|
<p class="mb-0 text-muted fst-italic" i18n>No bulk share links currently exist.</p>
|
||||||
}
|
}
|
||||||
@ -26,17 +31,42 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th scope="col" i18n>Created</th>
|
<th scope="col" i18n>Created</th>
|
||||||
<th scope="col" i18n>Status</th>
|
<th scope="col" i18n>Status</th>
|
||||||
|
<th scope="col" i18n>Size</th>
|
||||||
<th scope="col" i18n>Expires</th>
|
<th scope="col" i18n>Expires</th>
|
||||||
<th scope="col" i18n>Documents</th>
|
<th scope="col" i18n>Documents</th>
|
||||||
<th scope="col" i18n>Actions</th>
|
<th scope="col" i18n>File version</th>
|
||||||
|
<th scope="col" class="text-end" i18n>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (bundle of bundles; track bundle.id) {
|
@for (bundle of bundles; track bundle.id) {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ bundle.created | date: 'short' }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<span class="badge text-bg-secondary text-uppercase">{{ bundle.status }}</span>
|
<div>{{ bundle.created | date: 'short' }}</div>
|
||||||
|
@if (bundle.built_at) {
|
||||||
|
<div class="small text-muted">
|
||||||
|
<ng-container i18n>Built:</ng-container> {{ bundle.built_at | date: 'short' }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
@if (bundle.status === statuses.Processing || bundle.status === statuses.Pending) {
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||||
|
}
|
||||||
|
<span>{{ statusLabel(bundle.status) }}</span>
|
||||||
|
</div>
|
||||||
|
@if (bundle.last_error && bundle.status === statuses.Failed) {
|
||||||
|
<div class="small text-danger mt-1">{{ bundle.last_error }}</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (bundle.size_bytes !== undefined && bundle.size_bytes !== null) {
|
||||||
|
{{ bundle.size_bytes | fileSize }}
|
||||||
|
}
|
||||||
|
@if (bundle.size_bytes === undefined || bundle.size_bytes === null) {
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@if (bundle.expiration) {
|
@if (bundle.expiration) {
|
||||||
@ -47,11 +77,13 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ bundle.document_count }}</td>
|
<td>{{ bundle.document_count }}</td>
|
||||||
<td>
|
<td>{{ fileVersionLabel(bundle.file_version) }}</td>
|
||||||
|
<td class="text-end">
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-primary"
|
class="btn btn-outline-primary"
|
||||||
|
[disabled]="bundle.status !== statuses.Ready"
|
||||||
(click)="copy(bundle)"
|
(click)="copy(bundle)"
|
||||||
>
|
>
|
||||||
@if (copiedSlug === bundle.slug) {
|
@if (copiedSlug === bundle.slug) {
|
||||||
@ -62,9 +94,21 @@
|
|||||||
}
|
}
|
||||||
<span class="visually-hidden" i18n>Copy link</span>
|
<span class="visually-hidden" i18n>Copy link</span>
|
||||||
</button>
|
</button>
|
||||||
|
@if (bundle.status === statuses.Failed) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
[disabled]="loading"
|
||||||
|
(click)="retry(bundle)"
|
||||||
|
>
|
||||||
|
<i-bs name="arrow-clockwise"></i-bs>
|
||||||
|
<span class="visually-hidden" i18n>Retry</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-danger"
|
class="btn btn-outline-danger"
|
||||||
|
[disabled]="loading"
|
||||||
(click)="delete(bundle)"
|
(click)="delete(bundle)"
|
||||||
>
|
>
|
||||||
<i-bs name="trash"></i-bs>
|
<i-bs name="trash"></i-bs>
|
||||||
|
|||||||
@ -1,10 +1,15 @@
|
|||||||
import { Clipboard } from '@angular/cdk/clipboard'
|
import { Clipboard } from '@angular/cdk/clipboard'
|
||||||
import { CommonModule } from '@angular/common'
|
import { CommonModule } from '@angular/common'
|
||||||
import { Component, OnInit, inject } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { first } from 'rxjs'
|
import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
|
||||||
import { ShareBundleSummary } from 'src/app/data/share-bundle'
|
import {
|
||||||
|
ShareBundleStatus,
|
||||||
|
ShareBundleSummary,
|
||||||
|
} from 'src/app/data/share-bundle'
|
||||||
|
import { FileVersion } from 'src/app/data/share-link'
|
||||||
|
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
|
||||||
import { ShareBundleService } from 'src/app/services/rest/share-bundle.service'
|
import { ShareBundleService } from 'src/app/services/rest/share-bundle.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
@ -14,11 +19,11 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
selector: 'pngx-share-bundle-manage-dialog',
|
selector: 'pngx-share-bundle-manage-dialog',
|
||||||
templateUrl: './share-bundle-manage-dialog.component.html',
|
templateUrl: './share-bundle-manage-dialog.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, NgxBootstrapIconsModule],
|
imports: [CommonModule, NgxBootstrapIconsModule, FileSizePipe],
|
||||||
})
|
})
|
||||||
export class ShareBundleManageDialogComponent
|
export class ShareBundleManageDialogComponent
|
||||||
extends LoadingComponentWithPermissions
|
extends LoadingComponentWithPermissions
|
||||||
implements OnInit
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
private activeModal = inject(NgbActiveModal)
|
private activeModal = inject(NgbActiveModal)
|
||||||
private shareBundleService = inject(ShareBundleService)
|
private shareBundleService = inject(ShareBundleService)
|
||||||
@ -28,33 +33,70 @@ export class ShareBundleManageDialogComponent
|
|||||||
title = $localize`Bulk Share Links`
|
title = $localize`Bulk Share Links`
|
||||||
|
|
||||||
bundles: ShareBundleSummary[] = []
|
bundles: ShareBundleSummary[] = []
|
||||||
error: string
|
error: string | null = null
|
||||||
copiedSlug: string
|
copiedSlug: string | null = null
|
||||||
|
|
||||||
|
readonly statuses = ShareBundleStatus
|
||||||
|
readonly fileVersions = FileVersion
|
||||||
|
|
||||||
|
private readonly refresh$ = new Subject<boolean>()
|
||||||
|
|
||||||
|
private readonly statusLabels: Record<ShareBundleStatus, string> = {
|
||||||
|
[ShareBundleStatus.Pending]: $localize`Pending`,
|
||||||
|
[ShareBundleStatus.Processing]: $localize`Processing`,
|
||||||
|
[ShareBundleStatus.Ready]: $localize`Ready`,
|
||||||
|
[ShareBundleStatus.Failed]: $localize`Failed`,
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly fileVersionLabels: Record<FileVersion, string> = {
|
||||||
|
[FileVersion.Archive]: $localize`Archive`,
|
||||||
|
[FileVersion.Original]: $localize`Original`,
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.fetchBundles()
|
this.refresh$
|
||||||
|
.pipe(
|
||||||
|
switchMap((silent) => {
|
||||||
|
if (!silent) {
|
||||||
|
this.loading = true
|
||||||
|
}
|
||||||
|
this.error = null
|
||||||
|
return this.shareBundleService.listAllBundles().pipe(
|
||||||
|
catchError((error) => {
|
||||||
|
if (!silent) {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
this.error = $localize`Failed to load bulk share links.`
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error retrieving bulk share links.`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
return of(null)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
takeUntil(this.unsubscribeNotifier)
|
||||||
|
)
|
||||||
|
.subscribe((results) => {
|
||||||
|
if (results) {
|
||||||
|
this.bundles = results
|
||||||
|
this.copiedSlug = null
|
||||||
|
}
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.triggerRefresh(false)
|
||||||
|
timer(5000, 5000)
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => this.triggerRefresh(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchBundles(): void {
|
fetchBundles(): void {
|
||||||
this.loading = true
|
this.triggerRefresh(false)
|
||||||
this.error = null
|
|
||||||
this.shareBundleService
|
|
||||||
.listAllBundles()
|
|
||||||
.pipe(first())
|
|
||||||
.subscribe({
|
|
||||||
next: (results) => {
|
|
||||||
this.bundles = results
|
|
||||||
this.loading = false
|
|
||||||
},
|
|
||||||
error: (e) => {
|
|
||||||
this.loading = false
|
|
||||||
this.error = $localize`Failed to load bulk share links.`
|
|
||||||
this.toastService.showError(
|
|
||||||
$localize`Error retrieving bulk share links.`,
|
|
||||||
e
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getShareUrl(bundle: ShareBundleSummary): string {
|
getShareUrl(bundle: ShareBundleSummary): string {
|
||||||
@ -65,22 +107,29 @@ export class ShareBundleManageDialogComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
copy(bundle: ShareBundleSummary): void {
|
copy(bundle: ShareBundleSummary): void {
|
||||||
|
if (bundle.status !== ShareBundleStatus.Ready) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const success = this.clipboard.copy(this.getShareUrl(bundle))
|
const success = this.clipboard.copy(this.getShareUrl(bundle))
|
||||||
if (success) {
|
if (success) {
|
||||||
this.copiedSlug = bundle.slug
|
this.copiedSlug = bundle.slug
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.copiedSlug = null
|
this.copiedSlug = null
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
this.toastService.showInfo($localize`Share link copied to clipboard.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(bundle: ShareBundleSummary): void {
|
delete(bundle: ShareBundleSummary): void {
|
||||||
|
this.error = null
|
||||||
|
this.loading = true
|
||||||
this.shareBundleService.delete(bundle).subscribe({
|
this.shareBundleService.delete(bundle).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.showInfo($localize`Bulk share link deleted.`)
|
this.toastService.showInfo($localize`Bulk share link deleted.`)
|
||||||
this.fetchBundles()
|
this.triggerRefresh(false)
|
||||||
},
|
},
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
|
this.loading = false
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error deleting bulk share link.`,
|
$localize`Error deleting bulk share link.`,
|
||||||
e
|
e
|
||||||
@ -89,7 +138,47 @@ export class ShareBundleManageDialogComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
retry(bundle: ShareBundleSummary): void {
|
||||||
|
this.error = null
|
||||||
|
this.shareBundleService.rebuildBundle(bundle.id).subscribe({
|
||||||
|
next: (updated) => {
|
||||||
|
this.toastService.showInfo(
|
||||||
|
$localize`Bulk share link rebuild requested.`
|
||||||
|
)
|
||||||
|
this.replaceBundle(updated)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.toastService.showError($localize`Error requesting rebuild.`, e)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel(status: ShareBundleStatus): string {
|
||||||
|
return this.statusLabels[status] ?? status
|
||||||
|
}
|
||||||
|
|
||||||
|
fileVersionLabel(version: FileVersion): string {
|
||||||
|
return this.fileVersionLabels[version] ?? version
|
||||||
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private replaceBundle(updated: ShareBundleSummary): void {
|
||||||
|
const index = this.bundles.findIndex((bundle) => bundle.id === updated.id)
|
||||||
|
if (index >= 0) {
|
||||||
|
this.bundles = [
|
||||||
|
...this.bundles.slice(0, index),
|
||||||
|
updated,
|
||||||
|
...this.bundles.slice(index + 1),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
this.bundles = [updated, ...this.bundles]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private triggerRefresh(silent: boolean): void {
|
||||||
|
this.refresh$.next(silent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,13 @@ export class ShareBundleService extends AbstractNameFilterService<ShareBundleSum
|
|||||||
this.clearCache()
|
this.clearCache()
|
||||||
return this.http.post<ShareBundleSummary>(this.getResourceUrl(), payload)
|
return this.http.post<ShareBundleSummary>(this.getResourceUrl(), payload)
|
||||||
}
|
}
|
||||||
|
rebuildBundle(bundleId: number): Observable<ShareBundleSummary> {
|
||||||
|
this.clearCache()
|
||||||
|
return this.http.post<ShareBundleSummary>(
|
||||||
|
this.getResourceUrl(bundleId, 'rebuild'),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
listAllBundles(): Observable<ShareBundleSummary[]> {
|
listAllBundles(): Observable<ShareBundleSummary[]> {
|
||||||
return this.list(1, 1000, 'created', true).pipe(
|
return this.list(1, 1000, 'created', true).pipe(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user