From ea9838f13489b72539b60f47cc3701a847e36738 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:31:26 -0800 Subject: [PATCH] Frontend edit version label --- .../document-version-dropdown.component.html | 83 +++++++++++---- ...ocument-version-dropdown.component.spec.ts | 100 +++++++++++++++++- .../document-version-dropdown.component.ts | 77 ++++++++++++++ .../services/rest/document.service.spec.ts | 12 +++ .../src/app/services/rest/document.service.ts | 12 +++ 5 files changed, 261 insertions(+), 23 deletions(-) diff --git a/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html b/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html index 08545f0fd..1b4c3e1c9 100644 --- a/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html +++ b/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.html @@ -59,37 +59,76 @@ @for (version of versions; track version.id) { } diff --git a/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.spec.ts b/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.spec.ts index 3328152d6..c7075bbe0 100644 --- a/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.spec.ts @@ -17,7 +17,10 @@ describe('DocumentVersionDropdownComponent', () => { let component: DocumentVersionDropdownComponent let fixture: ComponentFixture let documentService: jest.Mocked< - Pick + Pick< + DocumentService, + 'deleteVersion' | 'getVersions' | 'uploadVersion' | 'updateVersionLabel' + > > let toastService: jest.Mocked> let finished$: Subject<{ taskId: string }> @@ -30,6 +33,7 @@ describe('DocumentVersionDropdownComponent', () => { deleteVersion: jest.fn(), getVersions: jest.fn(), uploadVersion: jest.fn(), + updateVersionLabel: jest.fn(), } toastService = { showError: jest.fn(), @@ -127,6 +131,96 @@ describe('DocumentVersionDropdownComponent', () => { ) }) + it('beginEditingVersion should set active row and draft label', () => { + component.userCanEdit = true + component.userIsOwner = true + const version = { + id: 10, + is_root: false, + checksum: 'bbbb', + version_label: 'Current', + } as DocumentVersionInfo + + component.beginEditingVersion(version) + + expect(component.editingVersionId).toEqual(10) + expect(component.versionLabelDraft).toEqual('Current') + }) + + it('submitEditedVersionLabel should close editor without save if unchanged', () => { + const version = { + id: 10, + is_root: false, + checksum: 'bbbb', + version_label: 'Current', + } as DocumentVersionInfo + const saveSpy = jest.spyOn(component, 'saveVersionLabel') + component.editingVersionId = 10 + component.versionLabelDraft = ' Current ' + + component.submitEditedVersionLabel(version) + + expect(saveSpy).not.toHaveBeenCalled() + expect(component.editingVersionId).toBeNull() + expect(component.versionLabelDraft).toEqual('') + }) + + it('submitEditedVersionLabel should call saveVersionLabel when changed', () => { + const version = { + id: 10, + is_root: false, + checksum: 'bbbb', + version_label: 'Current', + } as DocumentVersionInfo + const saveSpy = jest + .spyOn(component, 'saveVersionLabel') + .mockImplementation(() => {}) + component.editingVersionId = 10 + component.versionLabelDraft = ' Updated ' + + component.submitEditedVersionLabel(version) + + expect(saveSpy).toHaveBeenCalledWith(10, 'Updated') + expect(component.editingVersionId).toBeNull() + }) + + it('saveVersionLabel should update the version and emit versionsUpdated', () => { + documentService.updateVersionLabel.mockReturnValue( + of({ + id: 10, + version_label: 'Updated', + is_root: false, + } as any) + ) + const emitSpy = jest.spyOn(component.versionsUpdated, 'emit') + + component.saveVersionLabel(10, 'Updated') + + expect(documentService.updateVersionLabel).toHaveBeenCalledWith( + 3, + 10, + 'Updated' + ) + expect(emitSpy).toHaveBeenCalledWith([ + { id: 3, is_root: true, checksum: 'aaaa' }, + { id: 10, is_root: false, checksum: 'bbbb', version_label: 'Updated' }, + ]) + expect(component.savingVersionLabelId).toBeNull() + }) + + it('saveVersionLabel should show error toast on failure', () => { + const error = new Error('save failed') + documentService.updateVersionLabel.mockReturnValue(throwError(() => error)) + + component.saveVersionLabel(10, 'Updated') + + expect(toastService.showError).toHaveBeenCalledWith( + 'Error updating version label', + error + ) + expect(component.savingVersionLabelId).toBeNull() + }) + it('onVersionFileSelected should upload and update versions after websocket success', () => { const versions: DocumentVersionInfo[] = [ { id: 3, is_root: true, checksum: 'aaaa' }, @@ -215,6 +309,8 @@ describe('DocumentVersionDropdownComponent', () => { it('ngOnChanges should clear upload status on document switch', () => { component.versionUploadState = UploadState.Failed component.versionUploadError = 'something failed' + component.editingVersionId = 10 + component.versionLabelDraft = 'draft' component.ngOnChanges({ documentId: new SimpleChange(3, 4, false), @@ -222,5 +318,7 @@ describe('DocumentVersionDropdownComponent', () => { expect(component.versionUploadState).toEqual(UploadState.Idle) expect(component.versionUploadError).toBeNull() + expect(component.editingVersionId).toBeNull() + expect(component.versionLabelDraft).toEqual('') }) }) diff --git a/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.ts b/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.ts index 1a3c84d31..b96d865bc 100644 --- a/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.ts +++ b/src-ui/src/app/components/document-detail/document-version-dropdown/document-version-dropdown.component.ts @@ -15,6 +15,7 @@ import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { merge, of, Subject } from 'rxjs' import { filter, + finalize, first, map, switchMap, @@ -59,6 +60,9 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy { newVersionLabel: string = '' versionUploadState: UploadState = UploadState.Idle versionUploadError: string | null = null + savingVersionLabelId: number | null = null + editingVersionId: number | null = null + versionLabelDraft: string = '' private readonly documentsService = inject(DocumentService) private readonly toastService = inject(ToastService) @@ -70,6 +74,7 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy { if (changes.documentId && !changes.documentId.firstChange) { this.documentChange$.next() this.clearVersionUploadStatus() + this.cancelEditingVersion() } } @@ -84,6 +89,43 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy { this.versionSelected.emit(versionId) } + get canEditLabels(): boolean { + return this.userIsOwner && this.userCanEdit + } + + isEditingVersion(versionId: number): boolean { + return this.editingVersionId === versionId + } + + beginEditingVersion(version: DocumentVersionInfo, event?: Event): void { + event?.preventDefault() + event?.stopPropagation() + if (!this.canEditLabels || this.savingVersionLabelId !== null) return + this.editingVersionId = version.id + this.versionLabelDraft = version.version_label ?? '' + } + + cancelEditingVersion(event?: Event): void { + event?.preventDefault() + event?.stopPropagation() + this.editingVersionId = null + this.versionLabelDraft = '' + } + + submitEditedVersionLabel(version: DocumentVersionInfo, event?: Event): void { + event?.preventDefault() + event?.stopPropagation() + if (this.savingVersionLabelId !== null) return + const nextLabel = this.versionLabelDraft?.trim() || null + const currentLabel = version.version_label?.trim() || null + if (nextLabel === currentLabel) { + this.cancelEditingVersion() + return + } + this.saveVersionLabel(version.id, nextLabel) + this.cancelEditingVersion() + } + deleteVersion(versionId: number): void { const wasSelected = this.selectedVersionId === versionId this.documentsService @@ -114,6 +156,41 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy { }) } + saveVersionLabel(versionId: number, versionLabel: string | null): void { + if (this.savingVersionLabelId !== null) return + this.savingVersionLabelId = versionId + this.documentsService + .updateVersionLabel(this.documentId, versionId, versionLabel) + .pipe( + first(), + finalize(() => { + if (this.savingVersionLabelId === versionId) { + this.savingVersionLabelId = null + } + }), + takeUntil(this.destroy$) + ) + .subscribe({ + next: (updatedVersion) => { + const updatedVersions = this.versions.map((version) => + version.id === versionId + ? { + ...version, + version_label: updatedVersion.version_label, + } + : version + ) + this.versionsUpdated.emit(updatedVersions) + }, + error: (error) => { + this.toastService.showError( + $localize`Error updating version label`, + error + ) + }, + }) + } + onVersionFileSelected(event: Event): void { const input = event.target as HTMLInputElement if (!input?.files || input.files.length === 0) return diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts index 233755539..48c40e4d5 100644 --- a/src-ui/src/app/services/rest/document.service.spec.ts +++ b/src-ui/src/app/services/rest/document.service.spec.ts @@ -359,6 +359,18 @@ describe(`DocumentService`, () => { req.flush({ result: 'OK', current_version_id: documents[0].id }) }) + it('should call appropriate api endpoint for updating a document version label', () => { + subscription = service + .updateVersionLabel(documents[0].id, 10, 'Updated label') + .subscribe() + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/versions/10/` + ) + expect(req.request.method).toEqual('PATCH') + expect(req.request.body).toEqual({ version_label: 'Updated label' }) + req.flush({ id: 10, version_label: 'Updated label', is_root: false }) + }) + it('should call appropriate api endpoint for uploading a new version', () => { const file = new File(['hello'], 'test.pdf', { type: 'application/pdf' }) diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 41b7b2e22..ace34cc00 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -7,6 +7,7 @@ import { DOCUMENT_SORT_FIELDS, DOCUMENT_SORT_FIELDS_FULLTEXT, Document, + DocumentVersionInfo, } from 'src/app/data/document' import { DocumentMetadata } from 'src/app/data/document-metadata' import { DocumentSuggestions } from 'src/app/data/document-suggestions' @@ -245,6 +246,17 @@ export class DocumentService extends AbstractPaperlessService { ) } + updateVersionLabel( + rootDocumentId: number, + versionId: number, + versionLabel: string | null + ): Observable { + return this.http.patch( + this.getResourceUrl(rootDocumentId, `versions/${versionId}`), + { version_label: versionLabel } + ) + } + getNextAsn(): Observable { return this.http.get(this.getResourceUrl(null, 'next_asn')) }