mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-21 02:03:30 +00:00
Enhancement: merge documents as versions (#13515)
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{message}}</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="rootDocumentID" i18n>Root document:</label>
|
||||
<select id="rootDocumentID" class="form-select" [ngModel]="rootDocumentID()" (ngModelChange)="rootDocumentID.set($event)">
|
||||
@for (document of documents(); track document.id) {
|
||||
<option [ngValue]="document.id">{{document.title}}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<span class="form-label d-inline-block" i18n>Versions (oldest first):</span>
|
||||
<ul class="list-group"
|
||||
cdkDropList
|
||||
[cdkDropListData]="versionDocumentIDs()"
|
||||
(cdkDropListDropped)="onDrop($event)">
|
||||
@for (documentID of versionDocumentIDs(); track documentID) {
|
||||
@let document = getDocument(documentID);
|
||||
@if (document) {
|
||||
<li class="list-group-item d-flex align-items-center" cdkDrag>
|
||||
<i-bs name="grip-vertical" class="me-2"></i-bs>
|
||||
<div class="d-flex flex-column">
|
||||
<div>
|
||||
@if (document.correspondent) {
|
||||
<b>{{document.correspondent | correspondentName | async}}: </b>
|
||||
}{{document.title}}
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{{document.created | customDate:'mediumDate'}}
|
||||
@if (document.page_count) {
|
||||
| {document.page_count, plural, =1 {One page} other {{{document.page_count}} pages}}
|
||||
}
|
||||
</small>
|
||||
</div>
|
||||
@if ($last) {
|
||||
<span class="badge bg-primary ms-auto" i18n>Current version</span>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
@if (versionDocumentIDs().length > 1) {
|
||||
<div class="form-text" i18n>Drag to reorder.</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
|
||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled()">
|
||||
{{btnCaption}}
|
||||
</button>
|
||||
</div>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.list-group-item {
|
||||
cursor: move;
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { of } from 'rxjs'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { MergeAsVersionsConfirmDialogComponent } from './merge-as-versions-confirm-dialog.component'
|
||||
|
||||
describe('MergeAsVersionsConfirmDialogComponent', () => {
|
||||
let component: MergeAsVersionsConfirmDialogComponent
|
||||
let fixture: ComponentFixture<MergeAsVersionsConfirmDialogComponent>
|
||||
let documentService: DocumentService
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
MergeAsVersionsConfirmDialogComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(MergeAsVersionsConfirmDialogComponent)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should fetch selected documents', () => {
|
||||
const documents = [
|
||||
{ id: 1, title: 'Document 1' },
|
||||
{ id: 2, title: 'Document 2' },
|
||||
]
|
||||
jest.spyOn(documentService, 'getFew').mockReturnValue(
|
||||
of({
|
||||
all: [1, 2],
|
||||
count: 2,
|
||||
results: documents,
|
||||
})
|
||||
)
|
||||
component.documentIDs.set([1, 2])
|
||||
|
||||
component.ngOnInit()
|
||||
|
||||
expect(component.documents()).toEqual(documents)
|
||||
expect(documentService.getFew).toHaveBeenCalledWith([1, 2])
|
||||
})
|
||||
|
||||
it('should exclude the root from the draggable documents', () => {
|
||||
component.documentIDs.set([1, 2, 3])
|
||||
component.rootDocumentID.set(2)
|
||||
|
||||
expect(component.versionDocumentIDs()).toEqual([1, 3])
|
||||
})
|
||||
|
||||
it('should move draggable documents while keeping the root fixed', () => {
|
||||
component.documentIDs.set([1, 2, 3])
|
||||
component.rootDocumentID.set(1)
|
||||
|
||||
component.onDrop({ previousIndex: 1, currentIndex: 0 } as any)
|
||||
|
||||
expect(component.documentIDs()).toEqual([1, 3, 2])
|
||||
expect(component.versionDocumentIDs()).toEqual([3, 2])
|
||||
})
|
||||
})
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
CdkDragDrop,
|
||||
DragDropModule,
|
||||
moveItemInArray,
|
||||
} from '@angular/cdk/drag-drop'
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, OnInit, computed, inject, signal } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { CorrespondentNamePipe } from 'src/app/pipes/correspondent-name.pipe'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { ConfirmDialogComponent } from '../confirm-dialog.component'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-merge-as-versions-confirm-dialog',
|
||||
templateUrl: './merge-as-versions-confirm-dialog.component.html',
|
||||
styleUrl: './merge-as-versions-confirm-dialog.component.scss',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CorrespondentNamePipe,
|
||||
CustomDatePipe,
|
||||
DragDropModule,
|
||||
FormsModule,
|
||||
NgxBootstrapIconsModule,
|
||||
],
|
||||
})
|
||||
export class MergeAsVersionsConfirmDialogComponent
|
||||
extends ConfirmDialogComponent
|
||||
implements OnInit
|
||||
{
|
||||
private readonly documentService = inject(DocumentService)
|
||||
|
||||
readonly documentIDs = signal<number[]>([])
|
||||
readonly documents = signal<Document[]>([])
|
||||
readonly rootDocumentID = signal(-1)
|
||||
readonly versionDocumentIDs = computed(() =>
|
||||
this.documentIDs().filter(
|
||||
(documentID) => documentID !== this.rootDocumentID()
|
||||
)
|
||||
)
|
||||
|
||||
ngOnInit() {
|
||||
this.documentService
|
||||
.getFew(this.documentIDs())
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((response) => this.documents.set(response.results))
|
||||
}
|
||||
|
||||
onDrop(event: CdkDragDrop<number[]>) {
|
||||
const versionDocumentIDs = this.versionDocumentIDs().concat()
|
||||
moveItemInArray(versionDocumentIDs, event.previousIndex, event.currentIndex)
|
||||
|
||||
// The root keeps its place in the list, only the versions move around it
|
||||
let versionIndex = 0
|
||||
this.documentIDs.update((documentIDs) =>
|
||||
documentIDs.map((documentID) =>
|
||||
documentID === this.rootDocumentID()
|
||||
? documentID
|
||||
: versionDocumentIDs[versionIndex++]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getDocument(documentID: number): Document | undefined {
|
||||
return this.documents().find((document) => document.id === documentID)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
<div class="form-group mt-4">
|
||||
<label class="form-label" for="metadataDocumentID" i18n>Use metadata from:</label>
|
||||
<select class="form-select" [ngModel]="metadataDocumentID()" (ngModelChange)="metadataDocumentID.set($event)">
|
||||
<select id="metadataDocumentID" class="form-select" [ngModel]="metadataDocumentID()" (ngModelChange)="metadataDocumentID.set($event)">
|
||||
<option [ngValue]="-1" i18n>Regenerate all metadata</option>
|
||||
@for (document of documents(); track document.id) {
|
||||
<option [ngValue]="document.id">{{document.title}}</option>
|
||||
|
||||
Reference in New Issue
Block a user