mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-14 00:09:35 -06:00
OK extract versions to its own component
This commit is contained in:
@@ -24,89 +24,15 @@
|
||||
<i-bs width="1.2em" height="1.2em" name="trash"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
|
||||
</button>
|
||||
|
||||
@if (document?.versions?.length > 0) {
|
||||
<div class="btn-group" ngbDropdown autoClose="outside">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" ngbDropdownToggle>
|
||||
<i-bs name="file-earmark-diff"></i-bs>
|
||||
<span class="d-none d-lg-inline ps-1" i18n>Versions</span>
|
||||
</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<div class="px-3 py-2">
|
||||
@if (versionUploadState === UploadState.Idle) {
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<span class="input-group-text" i18n>Label</span>
|
||||
<input class="form-control" type="text" [(ngModel)]="newVersionLabel" i18n-placeholder placeholder="Optional" [disabled]="!userIsOwner || !userCanEdit" />
|
||||
</div>
|
||||
<input #versionFileInput type="file" class="visually-hidden" (change)="onVersionFileSelected($event)" />
|
||||
<button class="btn btn-sm btn-outline-secondary w-100" (click)="versionFileInput.click()" [disabled]="!userIsOwner || !userCanEdit">
|
||||
<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">
|
||||
<output class="spinner-border spinner-border-sm me-2" aria-hidden="true"></output>
|
||||
<span i18n>Uploading version...</span>
|
||||
</div>
|
||||
}
|
||||
@case (UploadState.Processing) {
|
||||
<div class="small text-muted mt-1 d-flex align-items-center">
|
||||
<output class="spinner-border spinner-border-sm me-2" aria-hidden="true"></output>
|
||||
<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 class="dropdown-divider"></div>
|
||||
@for (version of document.versions; track version.id) {
|
||||
<div class="dropdown-item">
|
||||
<div class="d-flex align-items-center w-100 version-item">
|
||||
<button type="button" class="btn btn-link link-underline link-underline-opacity-0 d-flex align-items-center flex-grow-1 small ps-0 text-start" (click)="selectVersion(version.id)">
|
||||
<div class="badge bg-light text-lowercase text-muted">
|
||||
{{ version.checksum | slice:0:8 }}
|
||||
</div>
|
||||
<div class="d-flex flex-column small ms-2">
|
||||
<div>
|
||||
@if (version.version_label) {
|
||||
{{ version.version_label }}
|
||||
} @else {
|
||||
<span i18n>ID</span> #{{version.id}}
|
||||
}
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
{{ version.added | customDate:'short' }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@if (selectedVersionId === version.id) { <span class="ms-2">✓</span> }
|
||||
@if (!version.is_root) {
|
||||
<pngx-confirm-button
|
||||
buttonClasses="btn-link btn-sm text-danger ms-2"
|
||||
iconName="trash"
|
||||
confirmMessage="Delete this version?"
|
||||
i18n-confirmMessage
|
||||
[disabled]="!userIsOwner || !userCanEdit"
|
||||
(confirm)="deleteVersion(version.id)"
|
||||
>
|
||||
<span class="visually-hidden" i18n>Delete version</span>
|
||||
</pngx-confirm-button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<pngx-document-version-dropdown
|
||||
[documentId]="documentId"
|
||||
[versions]="document?.versions ?? []"
|
||||
[selectedVersionId]="selectedVersionId"
|
||||
[userIsOwner]="userIsOwner"
|
||||
[userCanEdit]="userCanEdit"
|
||||
(versionSelected)="onVersionSelected($event)"
|
||||
(versionsUpdated)="onVersionsUpdated($event)"
|
||||
/>
|
||||
|
||||
<div class="btn-group">
|
||||
<button (click)="download()" class="btn btn-sm btn-outline-primary" [disabled]="downloading">
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { Subject, of, throwError } from 'rxjs'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
@@ -65,10 +65,6 @@ import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import {
|
||||
UploadState,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||
@@ -131,24 +127,6 @@ const customFields = [
|
||||
},
|
||||
]
|
||||
|
||||
function createFileInput(file?: File) {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
const files = file
|
||||
? ({
|
||||
0: file,
|
||||
length: 1,
|
||||
item: () => file,
|
||||
} as unknown as FileList)
|
||||
: ({
|
||||
length: 0,
|
||||
item: () => null,
|
||||
} as unknown as FileList)
|
||||
Object.defineProperty(input, 'files', { value: files })
|
||||
input.value = ''
|
||||
return input
|
||||
}
|
||||
|
||||
describe('DocumentDetailComponent', () => {
|
||||
let component: DocumentDetailComponent
|
||||
let fixture: ComponentFixture<DocumentDetailComponent>
|
||||
@@ -164,7 +142,6 @@ describe('DocumentDetailComponent', () => {
|
||||
let deviceDetectorService: DeviceDetectorService
|
||||
let httpTestingController: HttpTestingController
|
||||
let componentRouterService: ComponentRouterService
|
||||
let websocketStatusService: WebsocketStatusService
|
||||
|
||||
let currentUserCan = true
|
||||
let currentUserHasObjectPermissions = true
|
||||
@@ -314,7 +291,6 @@ describe('DocumentDetailComponent', () => {
|
||||
fixture = TestBed.createComponent(DocumentDetailComponent)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
componentRouterService = TestBed.inject(ComponentRouterService)
|
||||
websocketStatusService = TestBed.inject(WebsocketStatusService)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
@@ -1677,208 +1653,6 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(component.previewText).toContain('An error occurred loading content')
|
||||
})
|
||||
|
||||
it('deleteVersion should update versions, fall back, and surface errors', () => {
|
||||
initNormally()
|
||||
httpTestingController.expectOne(component.previewUrl).flush('preview')
|
||||
|
||||
component.document.versions = [
|
||||
{
|
||||
id: 3,
|
||||
added: new Date(),
|
||||
version_label: 'Original',
|
||||
checksum: 'aaaa',
|
||||
is_root: true,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
added: new Date(),
|
||||
version_label: 'Edited',
|
||||
checksum: 'bbbb',
|
||||
is_root: false,
|
||||
},
|
||||
]
|
||||
component.selectedVersionId = 10
|
||||
|
||||
const openDoc = { ...doc, versions: [] } as Document
|
||||
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc)
|
||||
const saveSpy = jest.spyOn(openDocumentsService, 'save')
|
||||
const deleteSpy = jest.spyOn(documentService, 'deleteVersion')
|
||||
const versionsSpy = jest.spyOn(documentService, 'getVersions')
|
||||
const selectSpy = jest
|
||||
.spyOn(component, 'selectVersion')
|
||||
.mockImplementation(() => {})
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
|
||||
deleteSpy.mockReturnValueOnce(of({ result: 'ok', current_version_id: 99 }))
|
||||
versionsSpy.mockReturnValueOnce(
|
||||
of({ id: doc.id, versions: [{ id: 99, is_root: false }] } as Document)
|
||||
)
|
||||
component.deleteVersion(10)
|
||||
|
||||
expect(component.document.versions).toEqual([{ id: 99, is_root: false }])
|
||||
expect(openDoc.versions).toEqual([{ id: 99, is_root: false }])
|
||||
expect(saveSpy).toHaveBeenCalled()
|
||||
expect(selectSpy).toHaveBeenCalledWith(99)
|
||||
|
||||
component.selectedVersionId = 3
|
||||
deleteSpy.mockReturnValueOnce(
|
||||
of({ result: 'ok', current_version_id: null })
|
||||
)
|
||||
versionsSpy.mockReturnValueOnce(
|
||||
of({
|
||||
id: doc.id,
|
||||
versions: [
|
||||
{ id: 7, is_root: false },
|
||||
{ id: 9, is_root: false },
|
||||
],
|
||||
} as Document)
|
||||
)
|
||||
component.deleteVersion(3)
|
||||
expect(selectSpy).toHaveBeenCalledWith(component.documentId)
|
||||
|
||||
deleteSpy.mockReturnValueOnce(throwError(() => new Error('nope')))
|
||||
component.deleteVersion(10)
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('onVersionFileSelected should cover upload flows and reset status', () => {
|
||||
initNormally()
|
||||
httpTestingController.expectOne(component.previewUrl).flush('preview')
|
||||
|
||||
const uploadSpy = jest.spyOn(documentService, 'uploadVersion')
|
||||
const versionsSpy = jest.spyOn(documentService, 'getVersions')
|
||||
const infoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||
const finishedSpy = jest.spyOn(
|
||||
websocketStatusService,
|
||||
'onDocumentConsumptionFinished'
|
||||
)
|
||||
const failedSpy = jest.spyOn(
|
||||
websocketStatusService,
|
||||
'onDocumentConsumptionFailed'
|
||||
)
|
||||
const selectSpy = jest
|
||||
.spyOn(component, 'selectVersion')
|
||||
.mockImplementation(() => {})
|
||||
const openDoc = { ...doc, versions: [] } as Document
|
||||
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc)
|
||||
const saveSpy = jest.spyOn(openDocumentsService, 'save')
|
||||
|
||||
component.onVersionFileSelected({ target: createFileInput() } as any)
|
||||
expect(uploadSpy).not.toHaveBeenCalled()
|
||||
|
||||
const fileMissing = new File(['data'], 'version.pdf', {
|
||||
type: 'application/pdf',
|
||||
})
|
||||
component.newVersionLabel = ' label '
|
||||
uploadSpy.mockReturnValueOnce(of({}))
|
||||
component.onVersionFileSelected({
|
||||
target: createFileInput(fileMissing),
|
||||
} as any)
|
||||
expect(uploadSpy).toHaveBeenCalledWith(
|
||||
component.documentId,
|
||||
fileMissing,
|
||||
'label'
|
||||
)
|
||||
expect(component.newVersionLabel).toBe('')
|
||||
expect(component.versionUploadState).toBe(UploadState.Failed)
|
||||
expect(component.versionUploadError).toBe('Missing task ID.')
|
||||
expect(infoSpy).toHaveBeenCalled()
|
||||
|
||||
const finishedFail$ = new Subject<any>()
|
||||
const failedFail$ = new Subject<any>()
|
||||
finishedSpy.mockReturnValueOnce(finishedFail$ as any)
|
||||
failedSpy.mockReturnValueOnce(failedFail$ as any)
|
||||
uploadSpy.mockReturnValueOnce(of('task-1'))
|
||||
component.onVersionFileSelected({
|
||||
target: createFileInput(
|
||||
new File(['data'], 'version.pdf', { type: 'application/pdf' })
|
||||
),
|
||||
} as any)
|
||||
expect(component.versionUploadState).toBe(UploadState.Processing)
|
||||
failedFail$.next({ taskId: 'task-1', message: 'nope' })
|
||||
expect(component.versionUploadState).toBe(UploadState.Failed)
|
||||
expect(component.versionUploadError).toBe('nope')
|
||||
expect(versionsSpy).not.toHaveBeenCalled()
|
||||
|
||||
const finishedOk$ = new Subject<any>()
|
||||
const failedOk$ = new Subject<any>()
|
||||
finishedSpy.mockReturnValueOnce(finishedOk$ as any)
|
||||
failedSpy.mockReturnValueOnce(failedOk$ as any)
|
||||
uploadSpy.mockReturnValueOnce(of({ task_id: 'task-2' }))
|
||||
const versions = [
|
||||
{ id: 7, is_root: false },
|
||||
{ id: 12, is_root: false },
|
||||
] as any
|
||||
versionsSpy.mockReturnValueOnce(of({ id: doc.id, versions } as Document))
|
||||
component.onVersionFileSelected({
|
||||
target: createFileInput(
|
||||
new File(['data'], 'version.pdf', { type: 'application/pdf' })
|
||||
),
|
||||
} as any)
|
||||
finishedOk$.next({ taskId: 'task-2' })
|
||||
|
||||
expect(component.document.versions).toEqual(versions)
|
||||
expect(openDoc.versions).toEqual(versions)
|
||||
expect(saveSpy).toHaveBeenCalled()
|
||||
expect(selectSpy).toHaveBeenCalledWith(12)
|
||||
expect(component.versionUploadState).toBe(UploadState.Idle)
|
||||
expect(component.versionUploadError).toBeNull()
|
||||
|
||||
component.versionUploadState = UploadState.Failed
|
||||
component.versionUploadError = 'boom'
|
||||
component.clearVersionUploadStatus()
|
||||
expect(component.versionUploadState).toBe(UploadState.Idle)
|
||||
expect(component.versionUploadError).toBeNull()
|
||||
|
||||
uploadSpy.mockReturnValueOnce(throwError(() => new Error('upload blew up')))
|
||||
component.onVersionFileSelected({
|
||||
target: createFileInput(
|
||||
new File(['data'], 'version.pdf', { type: 'application/pdf' })
|
||||
),
|
||||
} as any)
|
||||
expect(component.versionUploadState).toBe(UploadState.Failed)
|
||||
expect(component.versionUploadError).toBe('upload blew up')
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should clear and isolate version upload state on document change', () => {
|
||||
initNormally()
|
||||
httpTestingController.expectOne(component.previewUrl).flush('preview')
|
||||
|
||||
component.versionUploadState = UploadState.Failed
|
||||
component.versionUploadError = 'boom'
|
||||
component.docChangeNotifier.next(999)
|
||||
expect(component.versionUploadState).toBe(UploadState.Idle)
|
||||
expect(component.versionUploadError).toBeNull()
|
||||
|
||||
const uploadSpy = jest.spyOn(documentService, 'uploadVersion')
|
||||
const versionsSpy = jest.spyOn(documentService, 'getVersions')
|
||||
const finished$ = new Subject<any>()
|
||||
const failed$ = new Subject<any>()
|
||||
jest
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValueOnce(finished$ as any)
|
||||
jest
|
||||
.spyOn(websocketStatusService, 'onDocumentConsumptionFailed')
|
||||
.mockReturnValueOnce(failed$ as any)
|
||||
|
||||
uploadSpy.mockReturnValueOnce(of('task-stale'))
|
||||
component.onVersionFileSelected({
|
||||
target: createFileInput(
|
||||
new File(['data'], 'version.pdf', { type: 'application/pdf' })
|
||||
),
|
||||
} as any)
|
||||
expect(component.versionUploadState).toBe(UploadState.Processing)
|
||||
|
||||
component.docChangeNotifier.next(1000)
|
||||
failed$.next({ taskId: 'task-stale', message: 'stale-error' })
|
||||
|
||||
expect(component.versionUploadState).toBe(UploadState.Idle)
|
||||
expect(component.versionUploadError).toBeNull()
|
||||
expect(versionsSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('createDisabled should return true if the user does not have permission to add the specified data type', () => {
|
||||
currentUserCan = false
|
||||
expect(component.createDisabled(DataType.Correspondent)).toBeTruthy()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AsyncPipe, NgTemplateOutlet, SlicePipe } from '@angular/common'
|
||||
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
|
||||
import { HttpClient, HttpResponse } from '@angular/common/http'
|
||||
import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import {
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { DeviceDetectorService } from 'ngx-device-detector'
|
||||
import { BehaviorSubject, merge, Observable, of, Subject, timer } from 'rxjs'
|
||||
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs'
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
first,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
@@ -37,7 +36,7 @@ import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
|
||||
import { DataType } from 'src/app/data/datatype'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { Document, DocumentVersionInfo } from 'src/app/data/document'
|
||||
import { DocumentMetadata } from 'src/app/data/document-metadata'
|
||||
import { DocumentNote } from 'src/app/data/document-note'
|
||||
import { DocumentSuggestions } from 'src/app/data/document-suggestions'
|
||||
@@ -81,15 +80,10 @@ import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import {
|
||||
UploadState,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { getFilenameFromContentDisposition } from 'src/app/utils/http'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
import * as UTIF from 'utif'
|
||||
import { DocumentDetailFieldID } from '../admin/settings/settings.component'
|
||||
import { ConfirmButtonComponent } from '../common/confirm-button/confirm-button.component'
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||
import { PasswordRemovalConfirmDialogComponent } from '../common/confirm-dialog/password-removal-confirm-dialog/password-removal-confirm-dialog.component'
|
||||
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
|
||||
@@ -126,6 +120,7 @@ import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/sug
|
||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
import { DocumentHistoryComponent } from './document-history/document-history.component'
|
||||
import { DocumentVersionDropdownComponent } from './document-version-dropdown/document-version-dropdown.component'
|
||||
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
|
||||
|
||||
enum DocumentDetailNavIDs {
|
||||
@@ -183,8 +178,7 @@ enum ContentRenderType {
|
||||
TextAreaComponent,
|
||||
RouterModule,
|
||||
PngxPdfViewerComponent,
|
||||
ConfirmButtonComponent,
|
||||
SlicePipe,
|
||||
DocumentVersionDropdownComponent,
|
||||
],
|
||||
})
|
||||
export class DocumentDetailComponent
|
||||
@@ -192,7 +186,6 @@ export class DocumentDetailComponent
|
||||
implements OnInit, OnDestroy, DirtyComponent
|
||||
{
|
||||
PdfRenderMode = PdfRenderMode
|
||||
UploadState = UploadState
|
||||
|
||||
documentsService = inject(DocumentService)
|
||||
private route = inject(ActivatedRoute)
|
||||
@@ -215,7 +208,6 @@ export class DocumentDetailComponent
|
||||
private componentRouterService = inject(ComponentRouterService)
|
||||
private deviceDetectorService = inject(DeviceDetectorService)
|
||||
private savedViewService = inject(SavedViewService)
|
||||
private readonly websocketStatusService = inject(WebsocketStatusService)
|
||||
|
||||
@ViewChild('inputTitle')
|
||||
titleInput: TextComponent
|
||||
@@ -247,10 +239,7 @@ export class DocumentDetailComponent
|
||||
|
||||
// Versioning
|
||||
selectedVersionId: number
|
||||
newVersionLabel: string = ''
|
||||
pdfSource: PdfSource
|
||||
versionUploadState: UploadState = UploadState.Idle
|
||||
versionUploadError: string | null = null
|
||||
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
@@ -297,7 +286,6 @@ export class DocumentDetailComponent
|
||||
public readonly DocumentDetailFieldID = DocumentDetailFieldID
|
||||
|
||||
@ViewChild('nav') nav: NgbNav
|
||||
@ViewChild('versionFileInput') versionFileInput
|
||||
@ViewChild('pdfPreview') set pdfPreview(element) {
|
||||
// this gets called when component added or removed from DOM
|
||||
if (
|
||||
@@ -674,10 +662,6 @@ export class DocumentDetailComponent
|
||||
this.loadDocument(documentId)
|
||||
})
|
||||
|
||||
this.docChangeNotifier
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => this.clearVersionUploadStatus())
|
||||
|
||||
this.route.paramMap
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((paramMap) => {
|
||||
@@ -859,41 +843,17 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
deleteVersion(versionId: number) {
|
||||
const wasSelected = this.selectedVersionId === versionId
|
||||
this.documentsService
|
||||
.deleteVersion(this.documentId, versionId)
|
||||
.pipe(
|
||||
switchMap((result) =>
|
||||
this.documentsService
|
||||
.getVersions(this.documentId)
|
||||
.pipe(map((doc) => ({ doc, result })))
|
||||
),
|
||||
first(),
|
||||
takeUntil(this.unsubscribeNotifier)
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ doc, result }) => {
|
||||
if (doc?.versions) {
|
||||
this.document.versions = doc.versions
|
||||
const openDoc = this.openDocumentService.getOpenDocument(
|
||||
this.documentId
|
||||
)
|
||||
if (openDoc) {
|
||||
openDoc.versions = doc.versions
|
||||
this.openDocumentService.save()
|
||||
}
|
||||
}
|
||||
onVersionSelected(versionId: number) {
|
||||
this.selectVersion(versionId)
|
||||
}
|
||||
|
||||
if (wasSelected) {
|
||||
const fallbackId = result?.current_version_id ?? this.documentId
|
||||
this.selectVersion(fallbackId)
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError($localize`Error deleting version`, error)
|
||||
},
|
||||
})
|
||||
onVersionsUpdated(versions: DocumentVersionInfo[]) {
|
||||
this.document.versions = versions
|
||||
const openDoc = this.openDocumentService.getOpenDocument(this.documentId)
|
||||
if (openDoc) {
|
||||
openDoc.versions = versions
|
||||
this.openDocumentService.save()
|
||||
}
|
||||
}
|
||||
|
||||
get customFieldFormFields(): FormArray {
|
||||
@@ -1312,104 +1272,6 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
onVersionFileSelected(event: Event) {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (!input?.files || input.files.length === 0) return
|
||||
const uploadDocumentId = this.documentId
|
||||
const file = input.files[0]
|
||||
// Reset input to allow re-selection of the same file later
|
||||
input.value = ''
|
||||
const label = this.newVersionLabel?.trim()
|
||||
this.versionUploadState = UploadState.Uploading
|
||||
this.versionUploadError = null
|
||||
this.documentsService
|
||||
.uploadVersion(uploadDocumentId, file, label)
|
||||
.pipe(
|
||||
first(),
|
||||
tap(() => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Uploading new version. Processing will happen in the background.`
|
||||
)
|
||||
this.newVersionLabel = ''
|
||||
this.versionUploadState = UploadState.Processing
|
||||
}),
|
||||
map((taskId) =>
|
||||
typeof taskId === 'string'
|
||||
? taskId
|
||||
: (taskId as { task_id?: string })?.task_id
|
||||
),
|
||||
switchMap((taskId) => {
|
||||
if (!taskId) {
|
||||
this.versionUploadState = UploadState.Failed
|
||||
this.versionUploadError = $localize`Missing task ID.`
|
||||
return of(null)
|
||||
}
|
||||
return merge(
|
||||
this.websocketStatusService.onDocumentConsumptionFinished().pipe(
|
||||
filter((status) => status.taskId === taskId),
|
||||
map(() => ({ state: 'success' as const }))
|
||||
),
|
||||
this.websocketStatusService.onDocumentConsumptionFailed().pipe(
|
||||
filter((status) => status.taskId === taskId),
|
||||
map((status) => ({
|
||||
state: 'failed' as const,
|
||||
message: status.message,
|
||||
}))
|
||||
)
|
||||
).pipe(
|
||||
takeUntil(merge(this.unsubscribeNotifier, this.docChangeNotifier)),
|
||||
take(1)
|
||||
)
|
||||
}),
|
||||
switchMap((result) => {
|
||||
if (result?.state !== 'success') {
|
||||
if (result?.state === 'failed') {
|
||||
this.versionUploadState = UploadState.Failed
|
||||
this.versionUploadError =
|
||||
result.message || $localize`Upload failed.`
|
||||
}
|
||||
return of(null)
|
||||
}
|
||||
return this.documentsService.getVersions(uploadDocumentId)
|
||||
}),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
takeUntil(this.docChangeNotifier)
|
||||
)
|
||||
.subscribe({
|
||||
next: (doc) => {
|
||||
if (uploadDocumentId !== this.documentId) return
|
||||
if (doc?.versions) {
|
||||
this.document.versions = doc.versions
|
||||
const openDoc = this.openDocumentService.getOpenDocument(
|
||||
this.documentId
|
||||
)
|
||||
if (openDoc) {
|
||||
openDoc.versions = doc.versions
|
||||
this.openDocumentService.save()
|
||||
}
|
||||
this.selectVersion(
|
||||
Math.max(...doc.versions.map((version) => version.id))
|
||||
)
|
||||
this.clearVersionUploadStatus()
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
if (uploadDocumentId !== this.documentId) return
|
||||
this.versionUploadState = UploadState.Failed
|
||||
this.versionUploadError = error?.message || $localize`Upload failed.`
|
||||
this.toastService.showError(
|
||||
$localize`Error uploading new version`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
clearVersionUploadStatus() {
|
||||
this.versionUploadState = UploadState.Idle
|
||||
this.versionUploadError = null
|
||||
}
|
||||
|
||||
private getSelectedNonLatestVersionId(): number | null {
|
||||
const versions = this.document?.versions ?? []
|
||||
if (!versions.length || !this.selectedVersionId) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<div class="btn-group" ngbDropdown autoClose="outside">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" ngbDropdownToggle>
|
||||
<i-bs name="file-earmark-diff"></i-bs>
|
||||
<span class="d-none d-lg-inline ps-1" i18n>Versions</span>
|
||||
</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
<div class="px-3 py-2">
|
||||
@if (versionUploadState === UploadState.Idle) {
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<span class="input-group-text" i18n>Label</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
[(ngModel)]="newVersionLabel"
|
||||
i18n-placeholder
|
||||
placeholder="Optional"
|
||||
[disabled]="!userIsOwner || !userCanEdit"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
#versionFileInput
|
||||
type="file"
|
||||
class="visually-hidden"
|
||||
(change)="onVersionFileSelected($event)"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary w-100"
|
||||
(click)="versionFileInput.click()"
|
||||
[disabled]="!userIsOwner || !userCanEdit"
|
||||
>
|
||||
<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">
|
||||
<output class="spinner-border spinner-border-sm me-2" aria-hidden="true"></output>
|
||||
<span i18n>Uploading version...</span>
|
||||
</div>
|
||||
}
|
||||
@case (UploadState.Processing) {
|
||||
<div class="small text-muted mt-1 d-flex align-items-center">
|
||||
<output class="spinner-border spinner-border-sm me-2" aria-hidden="true"></output>
|
||||
<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 class="dropdown-divider"></div>
|
||||
@for (version of versions; track version.id) {
|
||||
<div class="dropdown-item">
|
||||
<div class="d-flex align-items-center w-100 version-item">
|
||||
<button type="button" class="btn btn-link link-underline link-underline-opacity-0 d-flex align-items-center flex-grow-1 small ps-0 text-start" (click)="selectVersion(version.id)">
|
||||
<div class="badge bg-light text-lowercase text-muted">
|
||||
{{ version.checksum | slice:0:8 }}
|
||||
</div>
|
||||
<div class="d-flex flex-column small ms-2">
|
||||
<div>
|
||||
@if (version.version_label) {
|
||||
{{ version.version_label }}
|
||||
} @else {
|
||||
<span i18n>ID</span> #{{version.id}}
|
||||
}
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
{{ version.added | customDate:'short' }}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
@if (selectedVersionId === version.id) { <span class="ms-2">✓</span> }
|
||||
@if (!version.is_root) {
|
||||
<pngx-confirm-button
|
||||
buttonClasses="btn-link btn-sm text-danger ms-2"
|
||||
iconName="trash"
|
||||
confirmMessage="Delete this version?"
|
||||
i18n-confirmMessage
|
||||
[disabled]="!userIsOwner || !userCanEdit"
|
||||
(confirm)="deleteVersion(version.id)"
|
||||
>
|
||||
<span class="visually-hidden" i18n>Delete version</span>
|
||||
</pngx-confirm-button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,192 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { SimpleChange } from '@angular/core'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { Subject, of, throwError } from 'rxjs'
|
||||
import { DocumentVersionInfo } from 'src/app/data/document'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import {
|
||||
UploadState,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { DocumentVersionDropdownComponent } from './document-version-dropdown.component'
|
||||
|
||||
describe('DocumentVersionDropdownComponent', () => {
|
||||
let component: DocumentVersionDropdownComponent
|
||||
let fixture: ComponentFixture<DocumentVersionDropdownComponent>
|
||||
let documentService: jest.Mocked<
|
||||
Pick<DocumentService, 'deleteVersion' | 'getVersions' | 'uploadVersion'>
|
||||
>
|
||||
let toastService: jest.Mocked<Pick<ToastService, 'showError' | 'showInfo'>>
|
||||
let finished$: Subject<{ taskId: string }>
|
||||
let failed$: Subject<{ taskId: string; message?: string }>
|
||||
|
||||
beforeEach(async () => {
|
||||
finished$ = new Subject<{ taskId: string }>()
|
||||
failed$ = new Subject<{ taskId: string; message?: string }>()
|
||||
documentService = {
|
||||
deleteVersion: jest.fn(),
|
||||
getVersions: jest.fn(),
|
||||
uploadVersion: jest.fn(),
|
||||
}
|
||||
toastService = {
|
||||
showError: jest.fn(),
|
||||
showInfo: jest.fn(),
|
||||
}
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
DocumentVersionDropdownComponent,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
],
|
||||
providers: [
|
||||
DatePipe,
|
||||
{
|
||||
provide: DocumentService,
|
||||
useValue: documentService,
|
||||
},
|
||||
{
|
||||
provide: SettingsService,
|
||||
useValue: {
|
||||
get: () => null,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: toastService,
|
||||
},
|
||||
{
|
||||
provide: WebsocketStatusService,
|
||||
useValue: {
|
||||
onDocumentConsumptionFinished: () => finished$,
|
||||
onDocumentConsumptionFailed: () => failed$,
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DocumentVersionDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
component.documentId = 3
|
||||
component.selectedVersionId = 3
|
||||
component.versions = [
|
||||
{
|
||||
id: 3,
|
||||
is_root: true,
|
||||
checksum: 'aaaa',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
is_root: false,
|
||||
checksum: 'bbbb',
|
||||
},
|
||||
]
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('selectVersion should emit the selected id', () => {
|
||||
const emitSpy = jest.spyOn(component.versionSelected, 'emit')
|
||||
component.selectVersion(10)
|
||||
expect(emitSpy).toHaveBeenCalledWith(10)
|
||||
})
|
||||
|
||||
it('deleteVersion should refresh versions and select fallback when deleting current selection', () => {
|
||||
const updatedVersions: DocumentVersionInfo[] = [
|
||||
{ id: 3, is_root: true, checksum: 'aaaa' },
|
||||
{ id: 20, is_root: false, checksum: 'cccc' },
|
||||
]
|
||||
component.selectedVersionId = 10
|
||||
documentService.deleteVersion.mockReturnValue(
|
||||
of({ result: 'deleted', current_version_id: 3 })
|
||||
)
|
||||
documentService.getVersions.mockReturnValue(
|
||||
of({ id: 3, versions: updatedVersions } as any)
|
||||
)
|
||||
const versionsEmitSpy = jest.spyOn(component.versionsUpdated, 'emit')
|
||||
const selectedEmitSpy = jest.spyOn(component.versionSelected, 'emit')
|
||||
|
||||
component.deleteVersion(10)
|
||||
|
||||
expect(documentService.deleteVersion).toHaveBeenCalledWith(3, 10)
|
||||
expect(documentService.getVersions).toHaveBeenCalledWith(3)
|
||||
expect(versionsEmitSpy).toHaveBeenCalledWith(updatedVersions)
|
||||
expect(selectedEmitSpy).toHaveBeenCalledWith(3)
|
||||
})
|
||||
|
||||
it('deleteVersion should show an error toast on failure', () => {
|
||||
const error = new Error('delete failed')
|
||||
documentService.deleteVersion.mockReturnValue(throwError(() => error))
|
||||
|
||||
component.deleteVersion(10)
|
||||
|
||||
expect(toastService.showError).toHaveBeenCalledWith(
|
||||
'Error deleting version',
|
||||
error
|
||||
)
|
||||
})
|
||||
|
||||
it('onVersionFileSelected should upload and update versions after websocket success', () => {
|
||||
const versions: DocumentVersionInfo[] = [
|
||||
{ id: 3, is_root: true, checksum: 'aaaa' },
|
||||
{ id: 20, is_root: false, checksum: 'cccc' },
|
||||
]
|
||||
const file = new File(['test'], 'new-version.pdf', {
|
||||
type: 'application/pdf',
|
||||
})
|
||||
const input = document.createElement('input')
|
||||
Object.defineProperty(input, 'files', { value: [file] })
|
||||
component.newVersionLabel = ' Updated scan '
|
||||
documentService.uploadVersion.mockReturnValue(
|
||||
of({ task_id: 'task-1' } as any)
|
||||
)
|
||||
documentService.getVersions.mockReturnValue(of({ id: 3, versions } as any))
|
||||
const versionsEmitSpy = jest.spyOn(component.versionsUpdated, 'emit')
|
||||
const selectedEmitSpy = jest.spyOn(component.versionSelected, 'emit')
|
||||
|
||||
component.onVersionFileSelected({ target: input } as Event)
|
||||
finished$.next({ taskId: 'task-1' })
|
||||
|
||||
expect(documentService.uploadVersion).toHaveBeenCalledWith(
|
||||
3,
|
||||
file,
|
||||
'Updated scan'
|
||||
)
|
||||
expect(toastService.showInfo).toHaveBeenCalled()
|
||||
expect(documentService.getVersions).toHaveBeenCalledWith(3)
|
||||
expect(versionsEmitSpy).toHaveBeenCalledWith(versions)
|
||||
expect(selectedEmitSpy).toHaveBeenCalledWith(20)
|
||||
expect(component.newVersionLabel).toEqual('')
|
||||
expect(component.versionUploadState).toEqual(UploadState.Idle)
|
||||
expect(component.versionUploadError).toBeNull()
|
||||
})
|
||||
|
||||
it('onVersionFileSelected should set failed state after websocket failure', () => {
|
||||
const file = new File(['test'], 'new-version.pdf', {
|
||||
type: 'application/pdf',
|
||||
})
|
||||
const input = document.createElement('input')
|
||||
Object.defineProperty(input, 'files', { value: [file] })
|
||||
documentService.uploadVersion.mockReturnValue(of('task-1'))
|
||||
|
||||
component.onVersionFileSelected({ target: input } as Event)
|
||||
failed$.next({ taskId: 'task-1', message: 'processing failed' })
|
||||
|
||||
expect(component.versionUploadState).toEqual(UploadState.Failed)
|
||||
expect(component.versionUploadError).toEqual('processing failed')
|
||||
expect(documentService.getVersions).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ngOnChanges should clear upload status on document switch', () => {
|
||||
component.versionUploadState = UploadState.Failed
|
||||
component.versionUploadError = 'something failed'
|
||||
|
||||
component.ngOnChanges({
|
||||
documentId: new SimpleChange(3, 4, false),
|
||||
})
|
||||
|
||||
expect(component.versionUploadState).toEqual(UploadState.Idle)
|
||||
expect(component.versionUploadError).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,203 @@
|
||||
import { SlicePipe } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { merge, of, Subject } from 'rxjs'
|
||||
import {
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
switchMap,
|
||||
take,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators'
|
||||
import { DocumentVersionInfo } from 'src/app/data/document'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import {
|
||||
UploadState,
|
||||
WebsocketStatusService,
|
||||
} from 'src/app/services/websocket-status.service'
|
||||
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-document-version-dropdown',
|
||||
templateUrl: './document-version-dropdown.component.html',
|
||||
imports: [
|
||||
FormsModule,
|
||||
NgbDropdownModule,
|
||||
NgxBootstrapIconsModule,
|
||||
ConfirmButtonComponent,
|
||||
SlicePipe,
|
||||
CustomDatePipe,
|
||||
],
|
||||
})
|
||||
export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy {
|
||||
UploadState = UploadState
|
||||
|
||||
@Input() documentId: number
|
||||
@Input() versions: DocumentVersionInfo[] = []
|
||||
@Input() selectedVersionId: number
|
||||
@Input() userCanEdit: boolean = false
|
||||
@Input() userIsOwner: boolean = false
|
||||
|
||||
@Output() versionSelected = new EventEmitter<number>()
|
||||
@Output() versionsUpdated = new EventEmitter<DocumentVersionInfo[]>()
|
||||
|
||||
newVersionLabel: string = ''
|
||||
versionUploadState: UploadState = UploadState.Idle
|
||||
versionUploadError: string | null = null
|
||||
|
||||
private readonly documentsService = inject(DocumentService)
|
||||
private readonly toastService = inject(ToastService)
|
||||
private readonly websocketStatusService = inject(WebsocketStatusService)
|
||||
private readonly destroy$ = new Subject<void>()
|
||||
private readonly documentChange$ = new Subject<void>()
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.documentId && !changes.documentId.firstChange) {
|
||||
this.documentChange$.next()
|
||||
this.clearVersionUploadStatus()
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.documentChange$.next()
|
||||
this.documentChange$.complete()
|
||||
this.destroy$.next()
|
||||
this.destroy$.complete()
|
||||
}
|
||||
|
||||
selectVersion(versionId: number): void {
|
||||
this.versionSelected.emit(versionId)
|
||||
}
|
||||
|
||||
deleteVersion(versionId: number): void {
|
||||
const wasSelected = this.selectedVersionId === versionId
|
||||
this.documentsService
|
||||
.deleteVersion(this.documentId, versionId)
|
||||
.pipe(
|
||||
switchMap((result) =>
|
||||
this.documentsService
|
||||
.getVersions(this.documentId)
|
||||
.pipe(map((doc) => ({ doc, result })))
|
||||
),
|
||||
first(),
|
||||
takeUntil(this.destroy$)
|
||||
)
|
||||
.subscribe({
|
||||
next: ({ doc, result }) => {
|
||||
if (doc?.versions) {
|
||||
this.versionsUpdated.emit(doc.versions)
|
||||
}
|
||||
|
||||
if (wasSelected || this.selectedVersionId === versionId) {
|
||||
const fallbackId = result?.current_version_id ?? this.documentId
|
||||
this.versionSelected.emit(fallbackId)
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.showError($localize`Error deleting version`, error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onVersionFileSelected(event: Event): void {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (!input?.files || input.files.length === 0) return
|
||||
const uploadDocumentId = this.documentId
|
||||
const file = input.files[0]
|
||||
input.value = ''
|
||||
const label = this.newVersionLabel?.trim()
|
||||
this.versionUploadState = UploadState.Uploading
|
||||
this.versionUploadError = null
|
||||
this.documentsService
|
||||
.uploadVersion(uploadDocumentId, file, label)
|
||||
.pipe(
|
||||
first(),
|
||||
tap(() => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Uploading new version. Processing will happen in the background.`
|
||||
)
|
||||
this.newVersionLabel = ''
|
||||
this.versionUploadState = UploadState.Processing
|
||||
}),
|
||||
map((taskId) =>
|
||||
typeof taskId === 'string'
|
||||
? taskId
|
||||
: (taskId as { task_id?: string })?.task_id
|
||||
),
|
||||
switchMap((taskId) => {
|
||||
if (!taskId) {
|
||||
this.versionUploadState = UploadState.Failed
|
||||
this.versionUploadError = $localize`Missing task ID.`
|
||||
return of(null)
|
||||
}
|
||||
return merge(
|
||||
this.websocketStatusService.onDocumentConsumptionFinished().pipe(
|
||||
filter((status) => status.taskId === taskId),
|
||||
map(() => ({ state: 'success' as const }))
|
||||
),
|
||||
this.websocketStatusService.onDocumentConsumptionFailed().pipe(
|
||||
filter((status) => status.taskId === taskId),
|
||||
map((status) => ({
|
||||
state: 'failed' as const,
|
||||
message: status.message,
|
||||
}))
|
||||
)
|
||||
).pipe(takeUntil(merge(this.destroy$, this.documentChange$)), take(1))
|
||||
}),
|
||||
switchMap((result) => {
|
||||
if (result?.state !== 'success') {
|
||||
if (result?.state === 'failed') {
|
||||
this.versionUploadState = UploadState.Failed
|
||||
this.versionUploadError =
|
||||
result.message || $localize`Upload failed.`
|
||||
}
|
||||
return of(null)
|
||||
}
|
||||
return this.documentsService.getVersions(uploadDocumentId)
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
takeUntil(this.documentChange$)
|
||||
)
|
||||
.subscribe({
|
||||
next: (doc) => {
|
||||
if (uploadDocumentId !== this.documentId) return
|
||||
if (doc?.versions) {
|
||||
this.versionsUpdated.emit(doc.versions)
|
||||
this.versionSelected.emit(
|
||||
Math.max(...doc.versions.map((version) => version.id))
|
||||
)
|
||||
this.clearVersionUploadStatus()
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
if (uploadDocumentId !== this.documentId) return
|
||||
this.versionUploadState = UploadState.Failed
|
||||
this.versionUploadError = error?.message || $localize`Upload failed.`
|
||||
this.toastService.showError(
|
||||
$localize`Error uploading new version`,
|
||||
error
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
clearVersionUploadStatus(): void {
|
||||
this.versionUploadState = UploadState.Idle
|
||||
this.versionUploadError = null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user