diff --git a/docs/usage.md b/docs/usage.md index 70aaf8b0a..279ed7cf7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -100,6 +100,7 @@ Think of versions as **file history** for a document. - In document detail, selecting a version switches the preview, file metadata and content (and download etc buttons) to that version. - Deleting a non-root version keeps metadata and falls back to the latest remaining version. - From the document list, select two or more documents and choose **Merge as versions** to combine them under one entry. Select the root document whose metadata and permissions should be retained; the other selected documents become file versions. The root may already have versions, but documents being added as versions must not have version histories of their own. +- From a document's **Versions** menu, choose **Existing** to search for another document and add it as a version of the current document. ### Management Lists diff --git a/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.html b/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.html new file mode 100644 index 000000000..c92993c93 --- /dev/null +++ b/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.html @@ -0,0 +1,18 @@ + + + diff --git a/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.spec.ts b/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.spec.ts new file mode 100644 index 000000000..8d8a20a2a --- /dev/null +++ b/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.spec.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { DocumentService } from 'src/app/services/rest/document.service' +import { AddExistingDocumentVersionDialogComponent } from './add-existing-document-version-dialog.component' + +describe('AddExistingDocumentVersionDialogComponent', () => { + let component: AddExistingDocumentVersionDialogComponent + let fixture: ComponentFixture + let activeModal: jest.Mocked> + + beforeEach(async () => { + activeModal = { dismiss: jest.fn() } + await TestBed.configureTestingModule({ + imports: [AddExistingDocumentVersionDialogComponent], + providers: [ + { + provide: NgbActiveModal, + useValue: activeModal, + }, + { + provide: DocumentService, + useValue: {}, + }, + ], + }).compileComponents() + + fixture = TestBed.createComponent(AddExistingDocumentVersionDialogComponent) + component = fixture.componentInstance + component.rootDocumentID = 3 + fixture.detectChanges() + }) + + it('should emit the single selected document', () => { + const emitSpy = jest.spyOn(component.confirmClicked, 'emit') + component.selectedDocumentIDs = [20] + + component.confirm() + + expect(emitSpy).toHaveBeenCalledWith(20) + }) + + it('should require exactly one selected document', () => { + const emitSpy = jest.spyOn(component.confirmClicked, 'emit') + component.selectedDocumentIDs = [20, 21] + + component.confirm() + + expect(emitSpy).not.toHaveBeenCalled() + }) + + it('should dismiss on cancel', () => { + component.cancel() + + expect(activeModal.dismiss).toHaveBeenCalled() + }) +}) diff --git a/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.ts b/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.ts new file mode 100644 index 000000000..ce967a4ad --- /dev/null +++ b/src-ui/src/app/components/document-detail/document-version-dropdown/add-existing-document-version-dialog/add-existing-document-version-dialog.component.ts @@ -0,0 +1,28 @@ +import { Component, EventEmitter, Input, Output, inject } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { DocumentLinkComponent } from 'src/app/components/common/input/document-link/document-link.component' + +@Component({ + selector: 'pngx-add-existing-document-version-dialog', + templateUrl: './add-existing-document-version-dialog.component.html', + imports: [DocumentLinkComponent, FormsModule], +}) +export class AddExistingDocumentVersionDialogComponent { + private readonly activeModal = inject(NgbActiveModal) + + @Input() rootDocumentID: number + @Output() confirmClicked = new EventEmitter() + + selectedDocumentIDs: number[] = [] + buttonsEnabled = true + + confirm(): void { + if (this.selectedDocumentIDs.length !== 1) return + this.confirmClicked.emit(this.selectedDocumentIDs[0]) + } + + cancel(): void { + this.activeModal.dismiss() + } +} 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 b37134618..9032a2e4a 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 @@ -24,13 +24,26 @@ class="visually-hidden" (change)="onVersionFileSelected($event)" /> - +
+ + +
} @else { @switch (versionUploadState()) { @case (UploadState.Uploading) { 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 b604b3738..9ec9b4e63 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 @@ -1,6 +1,7 @@ import { DatePipe } from '@angular/common' import { SimpleChange } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { Subject, of, throwError } from 'rxjs' import { DocumentVersionInfo } from 'src/app/data/document' @@ -19,12 +20,17 @@ describe('DocumentVersionDropdownComponent', () => { let documentService: jest.Mocked< Pick< DocumentService, - 'deleteVersion' | 'getVersions' | 'uploadVersion' | 'updateVersionLabel' + | 'deleteVersion' + | 'getVersions' + | 'mergeDocumentsAsVersions' + | 'uploadVersion' + | 'updateVersionLabel' > > let toastService: jest.Mocked> let finished$: Subject<{ taskId: string }> let failed$: Subject<{ taskId: string; message?: string }> + let modalService: jest.Mocked> beforeEach(async () => { finished$ = new Subject<{ taskId: string }>() @@ -32,9 +38,11 @@ describe('DocumentVersionDropdownComponent', () => { documentService = { deleteVersion: jest.fn(), getVersions: jest.fn(), + mergeDocumentsAsVersions: jest.fn(), uploadVersion: jest.fn(), updateVersionLabel: jest.fn(), } + modalService = { open: jest.fn() } toastService = { showError: jest.fn(), showInfo: jest.fn(), @@ -61,6 +69,10 @@ describe('DocumentVersionDropdownComponent', () => { provide: ToastService, useValue: toastService, }, + { + provide: NgbModal, + useValue: modalService, + }, { provide: WebsocketStatusService, useValue: { @@ -323,4 +335,43 @@ describe('DocumentVersionDropdownComponent', () => { expect(component.editingVersionId).toBeNull() expect(component.versionLabelDraft).toEqual('') }) + + it('addExistingDocumentAsVersion should merge with a label and refresh versions', () => { + const confirmClicked = new Subject() + const modal = { + componentInstance: { + rootDocumentID: null, + buttonsEnabled: true, + confirmClicked, + }, + close: jest.fn(), + } + modalService.open.mockReturnValue(modal as any) + documentService.mergeDocumentsAsVersions.mockReturnValue(of({} as any)) + const versions: DocumentVersionInfo[] = [ + { id: 3, is_root: true, checksum: 'aaaa' }, + { id: 20, is_root: false, checksum: 'cccc' }, + ] + documentService.getVersions.mockReturnValue(of({ id: 3, versions } as any)) + component.newVersionLabel = ' Imported ' + const versionsEmitSpy = jest.spyOn(component.versionsUpdated, 'emit') + const selectedEmitSpy = jest.spyOn(component.versionSelected, 'emit') + + component.addExistingDocumentAsVersion() + expect(modal.componentInstance.rootDocumentID).toEqual(3) + confirmClicked.next(20) + + expect(documentService.mergeDocumentsAsVersions).toHaveBeenCalledWith( + [3, 20], + 3, + 'Imported' + ) + expect(documentService.updateVersionLabel).not.toHaveBeenCalled() + expect(documentService.getVersions).toHaveBeenCalledWith(3) + expect(versionsEmitSpy).toHaveBeenCalledWith(versions) + expect(selectedEmitSpy).toHaveBeenCalledWith(20) + expect(component.newVersionLabel).toEqual('') + expect(modal.close).toHaveBeenCalled() + expect(toastService.showInfo).toHaveBeenCalled() + }) }) 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 d5b9052e2..236236948 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 @@ -11,7 +11,7 @@ import { SimpleChanges, } from '@angular/core' import { FormsModule } from '@angular/forms' -import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' +import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { merge, of, Subject } from 'rxjs' import { @@ -33,6 +33,7 @@ import { WebsocketStatusService, } from 'src/app/services/websocket-status.service' import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component' +import { AddExistingDocumentVersionDialogComponent } from './add-existing-document-version-dialog/add-existing-document-version-dialog.component' @Component({ selector: 'pngx-document-version-dropdown', @@ -69,6 +70,7 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy { private readonly documentsService = inject(DocumentService) private readonly toastService = inject(ToastService) private readonly websocketStatusService = inject(WebsocketStatusService) + private readonly modalService = inject(NgbModal) private readonly destroy$ = new Subject() private readonly documentChange$ = new Subject() @@ -278,6 +280,56 @@ export class DocumentVersionDropdownComponent implements OnChanges, OnDestroy { }) } + addExistingDocumentAsVersion(): void { + const modal = this.modalService.open( + AddExistingDocumentVersionDialogComponent, + { backdrop: 'static' } + ) + const dialog = + modal.componentInstance as AddExistingDocumentVersionDialogComponent + dialog.rootDocumentID = this.documentId + dialog.confirmClicked + .pipe(takeUntil(this.destroy$), takeUntil(this.documentChange$)) + .subscribe((existingDocumentID) => { + dialog.buttonsEnabled = false + const versionLabel = this.newVersionLabel?.trim() + this.documentsService + .mergeDocumentsAsVersions( + [this.documentId, existingDocumentID], + this.documentId, + versionLabel + ) + .pipe( + switchMap(() => this.documentsService.getVersions(this.documentId)), + first(), + finalize(() => (dialog.buttonsEnabled = true)), + takeUntil(this.destroy$), + takeUntil(this.documentChange$) + ) + .subscribe({ + next: (document) => { + if (document?.versions) { + this.versionsUpdated.emit(document.versions) + this.versionSelected.emit( + Math.max(...document.versions.map((version) => version.id)) + ) + } + this.newVersionLabel = '' + modal.close() + this.toastService.showInfo( + $localize`Existing document added as a version.` + ) + }, + error: (error) => { + this.toastService.showError( + $localize`Error adding existing document as a version`, + error + ) + }, + }) + }) + } + clearVersionUploadStatus(): void { this.versionUploadState.set(UploadState.Idle) this.versionUploadError.set(null)