Sweet, live updating

This commit is contained in:
shamoon
2026-02-10 13:13:21 -08:00
parent 3a5a32771e
commit 667e4b81eb
3 changed files with 84 additions and 16 deletions

View File

@@ -8,14 +8,40 @@
</button> </button>
<div class="dropdown-menu shadow" ngbDropdownMenu> <div class="dropdown-menu shadow" ngbDropdownMenu>
<div class="px-3 py-2"> <div class="px-3 py-2">
<div class="input-group input-group-sm mb-2"> @if (versionUploadState === UploadState.Idle) {
<span class="input-group-text" i18n>Label</span> <div class="input-group input-group-sm mb-2">
<input class="form-control" type="text" [(ngModel)]="newVersionLabel" i18n-placeholder placeholder="Optional" [disabled]="!userIsOwner || !userCanEdit" /> <span class="input-group-text" i18n>Label</span>
</div> <input class="form-control" type="text" [(ngModel)]="newVersionLabel" i18n-placeholder placeholder="Optional" [disabled]="!userIsOwner || !userCanEdit" />
<input #versionFileInput type="file" class="visually-hidden" (change)="onVersionFileSelected($event)" /> </div>
<button class="btn btn-sm btn-outline-secondary w-100" (click)="versionFileInput.click()" [disabled]="!userIsOwner || !userCanEdit"> <input #versionFileInput type="file" class="visually-hidden" (change)="onVersionFileSelected($event)" />
<i-bs name="file-earmark-plus"></i-bs><span class="ps-1" i18n>Add new version</span> <button class="btn btn-sm btn-outline-secondary w-100" (click)="versionFileInput.click()" [disabled]="!userIsOwner || !userCanEdit">
</button> <i-bs name="file-earmark-plus"></i-bs><span class="ps-1" i18n>Add new version</span>
</button>
} @else {
@switch (versionUploadState) {
@case (UploadState.Uploading) {
<div class="small text-muted mt-1 d-flex align-items-center">
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span i18n>Uploading version...</span>
</div>
}
@case (UploadState.Processing) {
<div class="small text-muted mt-1 d-flex align-items-center">
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
<span i18n>Processing version...</span>
</div>
}
@case (UploadState.Failed) {
<div class="small text-danger mt-1 d-flex align-items-center justify-content-between">
<span i18n>Version upload failed.</span>
<button type="button" class="btn btn-link btn-sm p-0 ms-2" (click)="clearVersionUploadStatus()" i18n>Dismiss</button>
</div>
@if (versionUploadError) {
<div class="small text-muted mt-1">{{ versionUploadError }}</div>
}
}
}
}
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@for (version of document.versions; track version.id) { @for (version of document.versions; track version.id) {

View File

@@ -20,7 +20,7 @@ import {
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DeviceDetectorService } from 'ngx-device-detector' import { DeviceDetectorService } from 'ngx-device-detector'
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs' import { BehaviorSubject, merge, Observable, of, Subject, timer } from 'rxjs'
import { import {
catchError, catchError,
debounceTime, debounceTime,
@@ -81,7 +81,10 @@ import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { WebsocketStatusService } from 'src/app/services/websocket-status.service' import {
UploadState,
WebsocketStatusService,
} from 'src/app/services/websocket-status.service'
import { getFilenameFromContentDisposition } from 'src/app/utils/http' import { getFilenameFromContentDisposition } from 'src/app/utils/http'
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter' import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
import * as UTIF from 'utif' import * as UTIF from 'utif'
@@ -188,6 +191,8 @@ export class DocumentDetailComponent
implements OnInit, OnDestroy, DirtyComponent implements OnInit, OnDestroy, DirtyComponent
{ {
PdfRenderMode = PdfRenderMode PdfRenderMode = PdfRenderMode
UploadState = UploadState
documentsService = inject(DocumentService) documentsService = inject(DocumentService)
private route = inject(ActivatedRoute) private route = inject(ActivatedRoute)
private tagService = inject(TagService) private tagService = inject(TagService)
@@ -237,6 +242,8 @@ export class DocumentDetailComponent
// Versioning: which document ID to use for file preview/download // Versioning: which document ID to use for file preview/download
selectedVersionId: number selectedVersionId: number
newVersionLabel: string = '' newVersionLabel: string = ''
versionUploadState: UploadState = UploadState.Idle
versionUploadError: string | null = null
previewText: string previewText: string
previewLoaded: boolean = false previewLoaded: boolean = false
tiffURL: string tiffURL: string
@@ -1239,6 +1246,8 @@ export class DocumentDetailComponent
// Reset input to allow re-selection of the same file later // Reset input to allow re-selection of the same file later
input.value = '' input.value = ''
const label = this.newVersionLabel?.trim() const label = this.newVersionLabel?.trim()
this.versionUploadState = UploadState.Uploading
this.versionUploadError = null
this.documentsService this.documentsService
.uploadVersion(this.documentId, file, label) .uploadVersion(this.documentId, file, label)
.pipe( .pipe(
@@ -1248,6 +1257,7 @@ export class DocumentDetailComponent
$localize`Uploading new version. Processing will happen in the background.` $localize`Uploading new version. Processing will happen in the background.`
) )
this.newVersionLabel = '' this.newVersionLabel = ''
this.versionUploadState = UploadState.Processing
}), }),
map((taskId) => map((taskId) =>
typeof taskId === 'string' typeof taskId === 'string'
@@ -1256,17 +1266,31 @@ export class DocumentDetailComponent
), ),
switchMap((taskId) => { switchMap((taskId) => {
if (!taskId) { if (!taskId) {
this.versionUploadState = UploadState.Failed
this.versionUploadError = $localize`Missing task ID.`
return of(null) return of(null)
} }
return this.websocketStatusService return merge(
.onDocumentConsumptionFinished() this.websocketStatusService.onDocumentConsumptionFinished().pipe(
.pipe(
filter((status) => status.taskId === taskId), filter((status) => status.taskId === taskId),
take(1) map(() => ({ state: 'success' as const }))
),
this.websocketStatusService.onDocumentConsumptionFailed().pipe(
filter((status) => status.taskId === taskId),
map((status) => ({
state: 'failed' as const,
message: status.message,
}))
) )
).pipe(take(1))
}), }),
switchMap((status) => { switchMap((result) => {
if (!status) { if (!result || result.state !== 'success') {
if (result?.state === 'failed') {
this.versionUploadState = UploadState.Failed
this.versionUploadError =
result.message || $localize`Upload failed.`
}
return of(null) return of(null)
} }
return this.documentsService.getVersions(this.documentId) return this.documentsService.getVersions(this.documentId)
@@ -1285,9 +1309,15 @@ export class DocumentDetailComponent
openDoc.versions = doc.versions openDoc.versions = doc.versions
this.openDocumentService.save() this.openDocumentService.save()
} }
this.selectVersion(
Math.max(...doc.versions.map((version) => version.id))
)
this.clearVersionUploadStatus()
} }
}, },
error: (error) => { error: (error) => {
this.versionUploadState = UploadState.Failed
this.versionUploadError = error?.message || $localize`Upload failed.`
this.toastService.showError( this.toastService.showError(
$localize`Error uploading new version`, $localize`Error uploading new version`,
error error
@@ -1296,6 +1326,11 @@ export class DocumentDetailComponent
}) })
} }
clearVersionUploadStatus() {
this.versionUploadState = UploadState.Idle
this.versionUploadError = null
}
download(original: boolean = false) { download(original: boolean = false) {
this.downloading = true this.downloading = true
const downloadUrl = this.documentsService.getDownloadUrl( const downloadUrl = this.documentsService.getDownloadUrl(

View File

@@ -89,6 +89,13 @@ export class FileStatus {
} }
} }
export enum UploadState {
Idle = 'idle',
Uploading = 'uploading',
Processing = 'processing',
Failed = 'failed',
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })