Merge pull request #789 from shamoon/feature/merge-tool-ui
Feature: merge tool UI
This commit is contained in:
29 files changed
+1073
-49
No files matched your search
Generated
+15
@@ -8361,6 +8361,21 @@
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"ngx-drag-drop": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-drag-drop/-/ngx-drag-drop-2.0.0.tgz",
|
||||
"integrity": "sha512-t+4/eiC8zaXKqU1ruNfFEfGs1GpMNwpffD0baopvZFKjQHCb5rhNqFilJ54wO4T0OwGp4/RnsVhlcxe1mX6UJg==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ngx-file-drop": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-file-drop/-/ngx-file-drop-10.0.0.tgz",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"ng2-pdf-viewer": "^6.3.2",
|
||||
"ngx-color": "^6.2.0",
|
||||
"ngx-cookie-service": "^10.1.1",
|
||||
"ngx-drag-drop": "^2.0.0",
|
||||
"ngx-file-drop": "^10.0.0",
|
||||
"ngx-infinite-scroll": "^9.1.0",
|
||||
"rxjs": "~6.6.0",
|
||||
|
||||
@@ -35,6 +35,7 @@ import { DocumentCardSmallComponent } from './components/document-list/document-
|
||||
import { BulkEditorComponent } from './components/document-list/bulk-editor/bulk-editor.component';
|
||||
import { NgxFileDropModule } from 'ngx-file-drop';
|
||||
import { TextComponent } from './components/common/input/text/text.component';
|
||||
import { InputDebounceComponent } from './components/common/input/debounce/input-debounce.component';
|
||||
import { SelectComponent } from './components/common/input/select/select.component';
|
||||
import { CheckComponent } from './components/common/input/check/check.component';
|
||||
import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component';
|
||||
@@ -66,6 +67,9 @@ import { ApiVersionInterceptor } from './interceptors/api-version.interceptor';
|
||||
import { ColorSliderModule } from 'ngx-color/slider';
|
||||
import { ColorComponent } from './components/common/input/color/color.component';
|
||||
import { SplitMergeComponent } from './components/split-merge/split-merge.component';
|
||||
import { DndModule } from 'ngx-drag-drop';
|
||||
import { DocumentChooserComponent } from './components/common/document-chooser/document-chooser.component';
|
||||
import { PageChooserComponent } from './components/common/page-chooser/page-chooser.component';
|
||||
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
@@ -120,6 +124,7 @@ registerLocaleData(localeEs)
|
||||
DocumentCardSmallComponent,
|
||||
BulkEditorComponent,
|
||||
TextComponent,
|
||||
InputDebounceComponent,
|
||||
SelectComponent,
|
||||
CheckComponent,
|
||||
SaveViewConfigDialogComponent,
|
||||
@@ -142,7 +147,9 @@ registerLocaleData(localeEs)
|
||||
DateComponent,
|
||||
ColorComponent,
|
||||
DocumentAsnComponent,
|
||||
SplitMergeComponent
|
||||
SplitMergeComponent,
|
||||
DocumentChooserComponent,
|
||||
PageChooserComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -155,7 +162,8 @@ registerLocaleData(localeEs)
|
||||
InfiniteScrollModule,
|
||||
PdfViewerModule,
|
||||
NgSelectModule,
|
||||
ColorSliderModule
|
||||
ColorSliderModule,
|
||||
DndModule
|
||||
],
|
||||
providers: [
|
||||
DatePipe,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title" i18n>Choose Documents</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="cancelClicked()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="m-n2 row row-cols-paperless-cards">
|
||||
<app-document-card-small [simpleCard]="true" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents"></app-document-card-small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-danger" (click)="cancelClicked()" i18n>Cancel</button>
|
||||
<button type="button" class="btn btn-outline-primary" [disabled]="list.selected.size == 0" (click)="confirmClicked.emit()" i18n>Add Selected</button>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
$paperless-card-breakpoints: (
|
||||
0: 2, // xs
|
||||
768px: 3, //md
|
||||
992px: 4, //lg
|
||||
1200px: 5, //xl
|
||||
1400px: 6, // xxl
|
||||
1600px: 7,
|
||||
1800px: 8,
|
||||
2000px: 9
|
||||
);
|
||||
|
||||
.row-cols-paperless-cards {
|
||||
@each $width, $n_cols in $paperless-card-breakpoints {
|
||||
@media(min-width: $width) {
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
width: 100% / $n_cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DocumentChooserComponent } from './document-chooser.component';
|
||||
|
||||
describe('DocumentChooserComponent', () => {
|
||||
let component: DocumentChooserComponent;
|
||||
let fixture: ComponentFixture<DocumentChooserComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DocumentChooserComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DocumentChooserComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-chooser',
|
||||
templateUrl: './document-chooser.component.html',
|
||||
styleUrls: ['./document-chooser.component.scss']
|
||||
})
|
||||
export class DocumentChooserComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
public list: DocumentListViewService
|
||||
) { }
|
||||
|
||||
@Output()
|
||||
public confirmClicked = new EventEmitter()
|
||||
|
||||
ngOnInit(): void {
|
||||
this.list.selectNone()
|
||||
this.list.activateSavedView(null)
|
||||
this.list.reload()
|
||||
}
|
||||
|
||||
cancelClicked() {
|
||||
this.list.selectNone()
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
||||
toggleSelected(d: PaperlessDocument, event: MouseEvent): void {
|
||||
if (!event.shiftKey) this.list.toggleSelected(d)
|
||||
else this.list.selectRangeTo(d)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<input type="text" [class]="cssClasses" [placeholder]="placeholder" [(ngModel)]="inputValue" (keyup)="keyUp($event)" />
|
||||
<span *ngIf="allowClear && inputValue?.length > 0" class="clear-wrapper" title="Clear all" (click)="clear()"><span aria-hidden="true" class="clear">×</span></span>
|
||||
@@ -0,0 +1,23 @@
|
||||
.clear-wrapper {
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
user-select: none;
|
||||
width: 17px;
|
||||
color: #999;
|
||||
|
||||
.clear {
|
||||
display: inline-block;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover .clear {
|
||||
color: #D0021B;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InputDebounceComponent } from './text.component';
|
||||
|
||||
describe('InputDebounceComponent', () => {
|
||||
let component: InputDebounceComponent;
|
||||
let fixture: ComponentFixture<InputDebounceComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ InputDebounceComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(InputDebounceComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Component, Input, Output, ElementRef, EventEmitter } from '@angular/core';
|
||||
import { fromEvent } from 'rxjs';
|
||||
import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-input-debounce',
|
||||
templateUrl: './input-debounce.component.html',
|
||||
styleUrls: ['./input-debounce.component.scss']
|
||||
})
|
||||
export class InputDebounceComponent {
|
||||
|
||||
@Input()
|
||||
placeholder: string
|
||||
|
||||
@Input()
|
||||
delay: number = 500
|
||||
|
||||
@Input()
|
||||
classes: string
|
||||
|
||||
@Input()
|
||||
allowClear: boolean = true
|
||||
|
||||
@Input()
|
||||
set pattern(regexStr: string) {
|
||||
this._pattern = new RegExp(regexStr)
|
||||
}
|
||||
|
||||
_pattern: RegExp
|
||||
|
||||
@Output()
|
||||
value: EventEmitter<string> = new EventEmitter<string>()
|
||||
|
||||
@Input()
|
||||
inputValue: string;
|
||||
|
||||
constructor(private elementRef: ElementRef) {
|
||||
fromEvent(elementRef.nativeElement, 'keyup').pipe(
|
||||
map(() => this.inputValue),
|
||||
debounceTime(this.delay),
|
||||
distinctUntilChanged()
|
||||
).subscribe(input =>
|
||||
this.value.emit(input)
|
||||
);
|
||||
}
|
||||
|
||||
keyUp(event: any) {
|
||||
if (!this._pattern) return
|
||||
if (event.key.length == 1 && !this._pattern.test(event.key)) { // e.g. dont do for backspace
|
||||
// invalid character, prevent input
|
||||
event.preventDefault()
|
||||
event.stopImmediatePropagation()
|
||||
this.inputValue = this.inputValue.slice(this.inputValue.length, 1)
|
||||
}
|
||||
}
|
||||
|
||||
get cssClasses(): string {
|
||||
return 'form-control ' + this.classes
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.inputValue = ''
|
||||
this.value.emit(this.inputValue)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{title}}: {{document.title}}</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="cancelClicked()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pdf-viewer class="page-chooser-viewer" [class.splitting]="splitting" [src]="previewUrl" [original-size]="false" [zoom]=".25" [autoresize]="true" [show-borders]="true" [show-all]="true" [render-text-mode]="2" (page-rendered)="afterPageRendered($event)" (after-load-complete)="pdfLoaded($event)"></pdf-viewer>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-danger" (click)="cancelClicked()" i18n>Cancel</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="confirmClicked()" [disabled]="pages.length == 0">{{title}}</button>
|
||||
</div>
|
||||
@@ -0,0 +1,89 @@
|
||||
@import "src/theme";
|
||||
|
||||
.modal-body {
|
||||
max-height: 70vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
::ng-deep .page-chooser-viewer {
|
||||
min-height: 400px;
|
||||
width: 100%;
|
||||
|
||||
.pdfViewer {
|
||||
display: flex !important;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
|
||||
.page:not(.disabled) {
|
||||
margin: 2px 3% 2rem !important;
|
||||
cursor: pointer;
|
||||
|
||||
.textLayer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.textLayer {
|
||||
background-color: $primary !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.splitting) {
|
||||
.pdfViewer {
|
||||
.page.selected {
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
display: block;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
content: "";
|
||||
background: $primary no-repeat 50%/50% 50%;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.textLayer {
|
||||
background-color: $primary !important;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.splitting {
|
||||
.pdfViewer {
|
||||
.page:not(:last-child).selected::after,
|
||||
.page:not(:last-child):not(.disabled):hover::after {
|
||||
background-color: $primary;
|
||||
width: 2rem;
|
||||
height: 100%;
|
||||
content: "Split Here";
|
||||
color: #abb2bf; // dark mode text color
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -2rem;
|
||||
writing-mode: vertical-lr;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1rem;
|
||||
font-size: 80%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.page.disabled {
|
||||
.canvasWrapper,
|
||||
.textLayer {
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PageChooserComponent } from './page-chooser.component';
|
||||
|
||||
describe('PageChooserComponent', () => {
|
||||
let component: PageChooserComponent;
|
||||
let fixture: ComponentFixture<PageChooserComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ PageChooserComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PageChooserComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { PaperlessDocument, PaperlessDocumentPart } from 'src/app/data/paperless-document';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-chooser',
|
||||
templateUrl: './page-chooser.component.html',
|
||||
styleUrls: ['./page-chooser.component.scss']
|
||||
})
|
||||
export class PageChooserComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private documentService: DocumentService
|
||||
) { }
|
||||
|
||||
get previewUrl(): string {
|
||||
return this.documentService.getPreviewUrl(this.document.id)
|
||||
}
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
@Input()
|
||||
splitting: boolean = false
|
||||
|
||||
public pages: number[]
|
||||
|
||||
public enabledPages: number[]
|
||||
|
||||
private nPages: number
|
||||
|
||||
@Output()
|
||||
public confirmPages = new EventEmitter<number[]>()
|
||||
|
||||
get title(): string {
|
||||
return this.splitting ? 'Select Split' : 'Select Pages'
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pages = (this.document as PaperlessDocumentPart).pages
|
||||
if (this.splitting && this.pages?.length) this.enabledPages = this.pages.concat([])
|
||||
if (!this.pages) this.pages = []
|
||||
}
|
||||
|
||||
cancelClicked() {
|
||||
this.activeModal.close()
|
||||
}
|
||||
|
||||
confirmClicked() {
|
||||
if (this.splitting) {
|
||||
// we need pages as the full range since we wont know length of document later
|
||||
const startPage = this.pages.pop()
|
||||
for (let page = startPage + 1; page <= this.nPages; page++) {
|
||||
if (!this.enabledPages || (this.enabledPages && this.enabledPages.indexOf(page) !== -1)) this.pages.push(page)
|
||||
}
|
||||
}
|
||||
this.confirmPages.emit(this.pages.sort((a, b) => { return a - b }))
|
||||
}
|
||||
|
||||
pdfLoaded(pdf: PDFDocumentProxy) {
|
||||
this.nPages = pdf.numPages
|
||||
}
|
||||
|
||||
public afterPageRendered(pageRenderedEvent: any) {
|
||||
const pageView = pageRenderedEvent.source /* as PDFPageView */
|
||||
const div = pageView.div as HTMLDivElement
|
||||
if (this.splitting && this.enabledPages?.length) {
|
||||
div.classList.toggle('disabled', this.enabledPages.indexOf(parseInt(div.dataset.pageNumber)) == -1)
|
||||
} else {
|
||||
div.classList.toggle('selected', this.pages.indexOf(parseInt(div.dataset.pageNumber)) !== -1)
|
||||
}
|
||||
div.onclick = () => {
|
||||
let pageNum: number = parseInt(div.dataset.pageNumber)
|
||||
if (this.splitting) { // only select 1 page
|
||||
if (pageNum == this.nPages || (this.enabledPages?.length && pageNum == Math.max(...this.enabledPages))) return
|
||||
this.pages = [pageNum]
|
||||
div.parentNode.childNodes.forEach(pageEl => {
|
||||
(pageEl as Element).classList.toggle('selected', false)
|
||||
})
|
||||
div.classList.toggle('selected', true)
|
||||
} else {
|
||||
this.pages.indexOf(pageNum) !== -1 ? this.pages.splice(this.pages.indexOf(pageNum), 1) : this.pages.push(pageNum)
|
||||
div.classList.toggle('selected', this.pages.indexOf(pageNum) !== -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-5
@@ -4,11 +4,6 @@ form {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
font-size: 80%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alerts-hidden {
|
||||
.btn {
|
||||
line-height: 1;
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="splitMerge()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#scissors" />
|
||||
</svg> <span class="d-none d-lg-inline" i18n>Split & merge</span>
|
||||
</svg> <span class="d-none d-lg-inline" i18n>Add to split/merge</span>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
|
||||
|
||||
@@ -56,6 +56,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto ml-auto mb-2 mb-xl-0 d-flex">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="addToSplitMerge()">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#scissors" />
|
||||
</svg> <span class="d-none d-lg-inline" i18n>Add to split/merge</span>
|
||||
</button>
|
||||
|
||||
<div class="btn-group btn-group-sm mr-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag';
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
|
||||
@@ -16,6 +16,8 @@ import { MatchingModel } from 'src/app/data/matching-model';
|
||||
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { SplitMergeService } from 'src/app/services/split-merge.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-editor',
|
||||
@@ -41,7 +43,9 @@ export class BulkEditorComponent {
|
||||
private modalService: NgbModal,
|
||||
private openDocumentService: OpenDocumentsService,
|
||||
private settings: SettingsService,
|
||||
private toastService: ToastService
|
||||
private toastService: ToastService,
|
||||
private splitMergeService: SplitMergeService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
applyOnClose: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)
|
||||
@@ -214,4 +218,9 @@ export class BulkEditorComponent {
|
||||
saveAs(result, 'documents.zip');
|
||||
})
|
||||
}
|
||||
|
||||
addToSplitMerge() {
|
||||
this.splitMergeService.addDocuments(this.list.selectedDocuments)
|
||||
this.router.navigate(["split_merge"])
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
||||
<div *ngIf="!simpleCard" style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
|
||||
<div *ngFor="let t of getTagsLimited$() | async">
|
||||
<app-tag [tag]="t" (click)="clickTag.emit(t.id);$event.stopPropagation()" [clickable]="true" linkTitle="Filter by tag" i18n-linkTitle></app-tag>
|
||||
</div>
|
||||
@@ -22,13 +22,13 @@
|
||||
|
||||
<div class="card-body p-2">
|
||||
<p class="card-text">
|
||||
<ng-container *ngIf="document.correspondent">
|
||||
<ng-container *ngIf="document.correspondent && !simpleCard">
|
||||
<a [routerLink]="" title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
|
||||
</ng-container>
|
||||
{{document.title | documentTitle}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer pt-0 pb-2 px-2">
|
||||
<div *ngIf="!simpleCard" class="card-footer pt-0 pb-2 px-2">
|
||||
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
|
||||
<button *ngIf="document.document_type" type="button" class="list-group-item list-group-item-action bg-transparent pl-0 p-1 border-0" title="Filter by document type"
|
||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||
|
||||
+7
-4
@@ -17,12 +17,15 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
@Input()
|
||||
selected = false
|
||||
|
||||
@Output()
|
||||
toggleSelected = new EventEmitter()
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
@Input()
|
||||
simpleCard: boolean = false
|
||||
|
||||
@Output()
|
||||
toggleSelected = new EventEmitter()
|
||||
|
||||
@Output()
|
||||
clickTag = new EventEmitter<number>()
|
||||
|
||||
@@ -93,6 +96,6 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
}
|
||||
|
||||
mouseLeaveCard() {
|
||||
this.popover.close()
|
||||
this.popover?.close()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,166 @@
|
||||
<app-page-header i18n-title title="Split & merge documents">
|
||||
<app-page-header i18n-title title="Split & Merge Editor">
|
||||
<div class="btn-group mt-2 mt-sm-0">
|
||||
<button class="btn btn-sm btn-outline-primary" i18n-title title="Add Documents" (click)="chooseDocuments()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
|
||||
</svg> <ng-container i18n>Add Documents</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group ml-2 mt-2 mt-sm-0">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
[popoverTitle]="optionsTitle" [autoClose]="'outside'" [ngbPopover]="optionsPopoverBody" popoverClass="shadow" #optionsPopover="ngbPopover">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#gear" />
|
||||
</svg> <ng-container i18n>Options</ng-container>
|
||||
</button>
|
||||
<ng-template #optionsTitle>
|
||||
<ng-container i18n>Options</ng-container>
|
||||
<button type="button" class="close" aria-label="Close" (click)="optionsPopover.close()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #optionsPopoverBody>
|
||||
<div class="form-group row text-dark">
|
||||
<label for="source_options" class="col-sm-3 col-form-label pt-0" i18n>Delete source documents</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="source_options-delete" [(ngModel)]="delete_source_documents">
|
||||
<label class="custom-control-label" for="source_options-delete" [class.text-danger]="delete_source_documents"><ng-container i18n>Delete</ng-container><br/><small [class.text-muted]="!delete_source_documents" i18n>Source documents will be immediately deleted.</small></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mt-4 row text-dark">
|
||||
<label for="metadata_options" class="col-sm-3 col-form-label pt-0" i18n>Output document metadata</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="metadata_setting-copy" name="metadata_setting" class="custom-control-input" [value]="splitMergeMetadata.COPY_FIRST" [(ngModel)]="metadata_setting">
|
||||
<label class="custom-control-label" for="metadata_setting-copy"><ng-container i18n>First document</ng-container><br/><small class="text-muted" i18n>Metadata will be copied from first document.</small></label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="metadata_setting-redo" name="metadata_setting" class="custom-control-input" [value]="splitMergeMetadata.REDO" [(ngModel)]="metadata_setting">
|
||||
<label class="custom-control-label" for="metadata_setting-redo"><ng-container i18n>Redo</ng-container><br/><small class="text-muted" i18n>Metadata detection will be re-run on new document.</small></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="btn-group ml-2 mr-2 mr-sm-5 mt-2 mt-sm-0">
|
||||
<button class="btn btn-sm btn-outline-primary" i18n-title title="Save" (click)="save()" [disabled]="splitMergeService.getDocuments().length < 1">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#check-circle" />
|
||||
</svg> <ng-container i18n>Save</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group ml-sm-5 mt-2 mt-sm-0">
|
||||
<button class="btn btn-sm btn-outline-danger" i18n-title title="Cancel" (click)="cancel()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
|
||||
</svg> <ng-container i18n>Cancel</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</app-page-header>
|
||||
|
||||
<p *ngFor="let d of documents">
|
||||
{{d.title}}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<div class="h-100" dndDropzone (dndDrop)="onDrop($event, documents)">
|
||||
<div class="spacer bg-primary" dndPlaceholderRef>
|
||||
</div>
|
||||
<div *ngFor="let d of documents; let i = index" class="card w-100 mb-3" [class.separator-card]="d.is_separator" [dndDraggable]="d" dndEffectAllowed="move" (dndMoved)="onDragged(d, documents)">
|
||||
<ng-container *ngIf="d.is_separator">
|
||||
<div class="separator"></div>
|
||||
<h6 class="text-muted small font-weight-bold d-inline-block py-2 px-3 mb-0 mx-auto bg-body" i18n>Document End</h6>
|
||||
<button class="btn btn-sm btn-danger" title="Remove" (click)="removeDocument(d, i)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg>
|
||||
</button>
|
||||
</ng-container>
|
||||
<div *ngIf="!d.is_separator" class="row no-gutters">
|
||||
<div class="col-md-3 bg-light d-flex handle" dndHandle>
|
||||
<svg width="2em" height="100%" viewBox="0 0 16 16" fill="currentColor" class="mx-auto">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grip-horizontal" />
|
||||
</svg>
|
||||
<div class="doc-thumb" [style.background-image]="'url(' + getThumbUrl(d.id) + ')'"></div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-2">{{d.title}}</h6>
|
||||
<div class="card-actions">
|
||||
<div class="btn-toolbar btn-toolbar-sm justify-content-between w-100">
|
||||
<button class="btn btn-sm btn-outline-danger flex-fill" i18n-title title="Remove" (click)="removeDocument(d, i)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#trash" />
|
||||
</svg><span class="d-none d-xl-inline"> <ng-container i18n>Remove</ng-container></span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary ml-3 flex-fill" i18n-title title="Split" (click)="chooseSplit(d, i)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#scissors" />
|
||||
</svg><span class="d-none d-xl-inline"> <ng-container i18n>Split</ng-container></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group input-group-sm w-100 mt-3">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text"><ng-container i18n>Pages</ng-container>:</div>
|
||||
</div>
|
||||
<app-input-debounce delay="500" classes="form-control-sm" placeholder="All" ngbTooltip="e.g. 1-3,5" pattern="[0-9\,\-]" [inputValue]="formatPages(d.pages)" (value)="pagesFieldChange($event, d, i)"></app-input-debounce>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-primary" type="button" i18n-title title="Select Pages" (click)="choosePages(d, i)">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
|
||||
</svg><span class="d-none d-xl-inline"> <ng-container i18n>Select Pages</ng-container></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-7">
|
||||
<ngb-alert *ngIf="error" type="danger" class="mt-2 mb-0" [dismissible]="false">
|
||||
<h6 class="alert-heading" i18n>An error occurred during operation</h6>
|
||||
<p class="mb-0 pb-1" i18n>Please check document settings and parts.</p>
|
||||
<p class="mb-0 pb-1 small"><ng-container i18n>Message</ng-container>: <span class="font-italic">{{error}}</span></p>
|
||||
</ngb-alert>
|
||||
<ng-container *ngIf="!error && previewUrls.length > 1">
|
||||
<ul ngbNav #previewNav="ngbNav" class="nav-tabs">
|
||||
<li *ngFor="let previewUrl of previewUrls; let j = index" [ngbNavItem]="j">
|
||||
<a class="bg-light" ngbNavLink><ng-container i18n>Document</ng-container> {{j + 1}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="preview-container px-2 tabbed text-center bg-light">
|
||||
<ng-container [ngTemplateOutlet]="previewHeader" [ngTemplateOutletContext]="{ $implicit: j }"></ng-container>
|
||||
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [render-text-mode]="2" [(page)]="previewCurrentPages[j]" (after-load-complete)="pdfPreviewLoaded($event, j)" (page-rendered)="pdfPageRendered($event, j)"></pdf-viewer>
|
||||
</div>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="previewNav"></div>
|
||||
</ng-container>
|
||||
<div *ngIf="!error && previewUrls.length <= 1" class="preview-container px-2 text-center bg-light">
|
||||
<ng-container [ngTemplateOutlet]="previewHeader" [ngTemplateOutletContext]="{ $implicit: 0 }"></ng-container>
|
||||
<pdf-viewer *ngIf="previewUrls.length > 0" [src]="previewUrls[0]" [original-size]="false" [show-borders]="true" [show-all]="true" [render-text-mode]="2" [(page)]="previewCurrentPages[0]" (after-load-complete)="pdfPreviewLoaded($event, 0)" (page-rendered)="pdfPageRendered($event, 0)"></pdf-viewer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" (click)="doIt()">Do it!</button>
|
||||
<ng-template #previewHeader let-index>
|
||||
<div class="d-flex mt-2 p-2 w-100 justify-space-between">
|
||||
<h6 class="text-muted mt-1" [class.mx-auto]="previewNumPages[index] == undefined" [class.mr-auto]="previewNumPages[index] !== undefined" i18n>Preview</h6>
|
||||
<div *ngIf="previewNumPages[index] !== undefined" class="input-group input-group-sm w-auto">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text" i18n>Page</div>
|
||||
</div>
|
||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages[index]" [(ngModel)]="previewCurrentPages[index]" />
|
||||
<div class="input-group-append">
|
||||
<div class="input-group-text" i18n>of {{previewNumPages[index]}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="loading" class="spinner-border" role="status">
|
||||
<span class="sr-only" i18n>Loading...</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,116 @@
|
||||
@import "/src/theme";
|
||||
|
||||
.card {
|
||||
.handle {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.doc-thumb {
|
||||
background-position: top center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
min-height: 5rem;
|
||||
height: 100%;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
&.separator-card {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
min-height: 0.25rem;
|
||||
width: 100%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
height: calc(100vh - 170px);
|
||||
overflow: hidden;
|
||||
|
||||
&.tabbed {
|
||||
height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
margin-top: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.split-document-preview ::ng-deep .ng2-pdf-viewer-container {
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
::ng-deep .document-selector {
|
||||
width: 80vw;
|
||||
min-width: 50vw;
|
||||
}
|
||||
|
||||
button span {
|
||||
margin-left: 0.15rem;
|
||||
}
|
||||
|
||||
app-input-debounce {
|
||||
flex: 1 1;
|
||||
position: relative;
|
||||
|
||||
::ng-deep .form-control {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link.bg-light {
|
||||
border-bottom-color: #f8f9fa;
|
||||
|
||||
&:not(.active) {
|
||||
background-color: #fff !important;
|
||||
border-bottom-color: #dee2e6;
|
||||
}
|
||||
}
|
||||
|
||||
.separator-card {
|
||||
.separator {
|
||||
border: 0;
|
||||
height: 20px;
|
||||
opacity: 0.5;
|
||||
background-color: transparent !important;
|
||||
background-image: url("data:image/svg+xml;utf8, <svg viewBox='0 0 200 110' xmlns='http://www.w3.org/2000/svg'><path d='M -15 110 L100 10 L215 110' fill='none' stroke='%23000000' stroke-width='1' vector-effect='non-scaling-stroke'/></svg>");
|
||||
background-position: bottom left;
|
||||
background-size: 2% auto;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
h6 {
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
letter-spacing: 0.1em;
|
||||
margin-top: -20px !important;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
button {
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:hover button {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .popover {
|
||||
min-width: 30rem;
|
||||
|
||||
.close {
|
||||
line-height: 0.7;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,212 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { SplitMergeMetadata } from 'src/app/data/split-merge-request';
|
||||
import { SplitMergeService } from 'src/app/services/split-merge.service';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { PaperlessDocument, PaperlessDocumentPart } from 'src/app/data/paperless-document';
|
||||
import { DndDropEvent } from 'ngx-drag-drop';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DocumentChooserComponent } from 'src/app/components/common/document-chooser/document-chooser.component'
|
||||
import { PageChooserComponent } from 'src/app/components/common/page-chooser/page-chooser.component'
|
||||
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
|
||||
|
||||
@Component({
|
||||
selector: 'app-split-merge',
|
||||
templateUrl: './split-merge.component.html',
|
||||
styleUrls: ['./split-merge.component.scss']
|
||||
})
|
||||
export class SplitMergeComponent implements OnInit {
|
||||
export class SplitMergeComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(private splitMergeService: SplitMergeService, private router: Router) { }
|
||||
public loading: boolean = false
|
||||
|
||||
ngOnInit(): void {
|
||||
public previewUrls: string[] = []
|
||||
public previewNumPages: number[] = []
|
||||
public previewCurrentPages: number[] = []
|
||||
|
||||
public delete_source_documents: boolean = false
|
||||
|
||||
public metadata_setting: SplitMergeMetadata = SplitMergeMetadata.COPY_FIRST
|
||||
|
||||
private previewDebounce$ = new Subject()
|
||||
|
||||
private previewSub: Subscription
|
||||
|
||||
public error: string
|
||||
|
||||
constructor(
|
||||
public splitMergeService: SplitMergeService,
|
||||
private documentService: DocumentService,
|
||||
private list: DocumentListViewService,
|
||||
private router: Router,
|
||||
private modalService: NgbModal
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.previewSub = this.previewDebounce$.pipe(
|
||||
debounceTime(400)
|
||||
).subscribe(() => {
|
||||
if (this.splitMergeService.hasDocuments()) {
|
||||
this.save(true)
|
||||
} else {
|
||||
this.previewUrls = []
|
||||
this.error = undefined
|
||||
}
|
||||
})
|
||||
this.previewDebounce$.next()
|
||||
}
|
||||
|
||||
get documents() {
|
||||
ngOnDestroy() {
|
||||
this.previewDebounce$.complete()
|
||||
}
|
||||
|
||||
get documents(): PaperlessDocument[] {
|
||||
return this.splitMergeService.getDocuments()
|
||||
}
|
||||
|
||||
doIt() {
|
||||
this.splitMergeService.executeSplitMerge(false, false, SplitMergeMetadata.COPY_FIRST).subscribe(
|
||||
getThumbUrl(documentId: number) {
|
||||
return this.documentService.getThumbUrl(documentId)
|
||||
}
|
||||
|
||||
get splitMergeMetadata() {
|
||||
return SplitMergeMetadata
|
||||
}
|
||||
|
||||
chooseDocuments() {
|
||||
let modal = this.modalService.open(DocumentChooserComponent, { backdrop: 'static', size: 'xl' })
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
this.splitMergeService.addDocuments(this.list.selectedDocuments)
|
||||
this.list.selectNone()
|
||||
modal.close()
|
||||
this.previewDebounce$.next()
|
||||
})
|
||||
}
|
||||
|
||||
onDragged(document: PaperlessDocument, documents: PaperlessDocument[]) {
|
||||
const index = this.documents.indexOf(document)
|
||||
this.documents.splice(index, 1)
|
||||
}
|
||||
|
||||
onDrop(event: DndDropEvent, documents: PaperlessDocument[]) {
|
||||
let index = event.index
|
||||
if (typeof index === "undefined") {
|
||||
index = this.documents.length
|
||||
}
|
||||
this.documents.splice(index, 0, event.data)
|
||||
this.previewDebounce$.next()
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.splitMergeService.clear()
|
||||
this.router.navigate([""])
|
||||
}
|
||||
|
||||
save(preview: boolean = false) {
|
||||
this.loading = true
|
||||
this.previewUrls = []
|
||||
this.splitMergeService.executeSplitMerge(preview, this.delete_source_documents, this.metadata_setting).subscribe(
|
||||
result => {
|
||||
console.log(result)
|
||||
this.splitMergeService.clear()
|
||||
this.router.navigate([""])
|
||||
this.loading = false
|
||||
this.error = undefined
|
||||
if (preview) {
|
||||
this.previewUrls = result.map(r => this.splitMergeService.getPreviewUrl(r))
|
||||
} else {
|
||||
this.splitMergeService.clear()
|
||||
this.router.navigate([""])
|
||||
}
|
||||
},
|
||||
error => {
|
||||
this.error = error
|
||||
this.loading = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
removeDocument(d: PaperlessDocument, index: number) {
|
||||
this.splitMergeService.removeDocument(d, index)
|
||||
this.previewNumPages[index] = this.previewCurrentPages[index] = undefined
|
||||
this.previewDebounce$.next()
|
||||
}
|
||||
|
||||
choosePages(d: PaperlessDocument, index: number) {
|
||||
let modal = this.modalService.open(PageChooserComponent, { backdrop: 'static', size: 'lg' })
|
||||
modal.componentInstance.document = d
|
||||
modal.componentInstance.confirmPages.subscribe((pages) => {
|
||||
this.splitMergeService.setDocumentPages(d, index, pages)
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.close()
|
||||
this.previewDebounce$.next()
|
||||
})
|
||||
}
|
||||
|
||||
chooseSplit(d: PaperlessDocument, index: number) {
|
||||
let modal = this.modalService.open(PageChooserComponent, { backdrop: 'static', size: 'lg' })
|
||||
modal.componentInstance.document = d
|
||||
const enabledPages = (d as PaperlessDocumentPart).pages
|
||||
modal.componentInstance.splitting = true
|
||||
modal.componentInstance.confirmPages.subscribe((pages) => {
|
||||
this.splitMergeService.splitDocument(d, index, pages, enabledPages)
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.close()
|
||||
this.previewDebounce$.next()
|
||||
})
|
||||
}
|
||||
|
||||
pagesFieldChange(pageStr: string, d: PaperlessDocument, index: number) {
|
||||
let pages = pageStr.split(',').map(p => {
|
||||
if (p.indexOf('-') !== -1) {
|
||||
const minmax = p.split('-')
|
||||
let range = []
|
||||
for (let i = parseInt(minmax[0]); i <= parseInt(minmax[1]); i++) {
|
||||
range.push(i)
|
||||
}
|
||||
return range
|
||||
} else if (p.length > 0) {
|
||||
return parseInt(p)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
pages = [].concat.apply([], pages) // e.g. flat()
|
||||
pages = pages.filter(page => page !== null)
|
||||
this.splitMergeService.setDocumentPages(d, index, pages as number[])
|
||||
this.previewDebounce$.next()
|
||||
}
|
||||
|
||||
formatPages(pages: number[]): string {
|
||||
let pageStrings = []
|
||||
let rangeStart, rangeEnd
|
||||
|
||||
pages?.forEach(page => {
|
||||
if (rangeStart == undefined) {
|
||||
rangeStart = page
|
||||
} else if (page - rangeStart == 1) {
|
||||
rangeEnd = page
|
||||
} else if (rangeEnd !== undefined && page - rangeEnd == 1) {
|
||||
rangeEnd = page
|
||||
} else {
|
||||
pageStrings.push(rangeEnd !== undefined ? rangeStart + '-' + rangeEnd : rangeStart)
|
||||
rangeStart = page
|
||||
rangeEnd = undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (rangeEnd !== undefined) {
|
||||
pageStrings.push(rangeStart + '-' + rangeEnd)
|
||||
} else if (rangeStart !== undefined) {
|
||||
pageStrings.push(rangeStart)
|
||||
}
|
||||
return pageStrings.join(',')
|
||||
}
|
||||
|
||||
pdfPreviewLoaded(pdf: PDFDocumentProxy, index: number) {
|
||||
this.previewNumPages[index] = pdf.numPages
|
||||
}
|
||||
|
||||
pdfPageRendered(event: any, index) { // CustomEvent is es6
|
||||
if (event.pageNumber > 0 && this.previewCurrentPages[index] == undefined) this.previewCurrentPages[index] = 1
|
||||
}
|
||||
}
|
||||
@@ -41,3 +41,11 @@ export interface PaperlessDocument extends ObjectWithId {
|
||||
archive_serial_number?: number
|
||||
|
||||
}
|
||||
|
||||
export interface PaperlessDocumentPart extends PaperlessDocument {
|
||||
pages: number[]
|
||||
}
|
||||
|
||||
export interface PaperlessDocumentSeparator extends PaperlessDocument {
|
||||
is_separator: boolean
|
||||
}
|
||||
@@ -181,6 +181,10 @@ export class DocumentListViewService {
|
||||
return this.activeListViewState.selected
|
||||
}
|
||||
|
||||
get selectedDocuments(): PaperlessDocument[] {
|
||||
return this.activeListViewState.documents.filter(d => this.activeListViewState.selected.has(d.id))
|
||||
}
|
||||
|
||||
setSort(field: string, reverse: boolean) {
|
||||
this.activeListViewState.sortField = field
|
||||
this.activeListViewState.sortReverse = reverse
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { PaperlessDocument } from '../data/paperless-document';
|
||||
import { PaperlessDocument, PaperlessDocumentPart, PaperlessDocumentSeparator } from '../data/paperless-document';
|
||||
import { SplitMergeMetadata, SplitMergeRequest } from '../data/split-merge-request';
|
||||
|
||||
@Injectable({
|
||||
@@ -15,35 +16,84 @@ export class SplitMergeService {
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
// just a demo
|
||||
addDocument(document: PaperlessDocument) {
|
||||
this.documents.push(document)
|
||||
addDocument(document: PaperlessDocument, atIndex?: number) {
|
||||
if (atIndex !== undefined) this.documents.splice(atIndex, 0, document)
|
||||
else this.documents.push(document)
|
||||
}
|
||||
|
||||
addDocuments(documents: PaperlessDocument[]) {
|
||||
this.documents = this.documents.concat(documents)
|
||||
}
|
||||
|
||||
removeDocument(document: PaperlessDocument, atIndex?: number) {
|
||||
if (!atIndex) atIndex = this.documents.indexOf(document)
|
||||
this.documents.splice(atIndex, 1)
|
||||
this.sanitizeStartEndSeparators()
|
||||
}
|
||||
|
||||
getDocuments() {
|
||||
return this.documents
|
||||
}
|
||||
|
||||
// just a demo
|
||||
hasDocuments(): boolean {
|
||||
return this.documents.length > 0
|
||||
}
|
||||
|
||||
// just a demo
|
||||
getDocuments() {
|
||||
return this.documents
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.documents = []
|
||||
}
|
||||
|
||||
setDocumentPages(d: PaperlessDocument, index: number, pages: number[]) {
|
||||
(this.documents[index] as PaperlessDocumentPart).pages = pages.length > 0 ? pages : null
|
||||
}
|
||||
|
||||
splitDocument(d: PaperlessDocument, index: number, secondPages: number[], enabledPages?: number[]) {
|
||||
const firstPages = []
|
||||
for (let page = 1; page < secondPages[0]; page++) {
|
||||
if (!enabledPages || (enabledPages?.length && enabledPages.indexOf(page) !== -1)) firstPages.push(page)
|
||||
}
|
||||
(this.documents[index] as PaperlessDocumentPart).pages = firstPages
|
||||
this.documents.splice(index + 1, 0, { is_separator: true } as PaperlessDocumentSeparator)
|
||||
let d2 = { ...d } as PaperlessDocumentPart
|
||||
d2.pages = secondPages
|
||||
this.documents.splice(index + 2, 0, d2)
|
||||
}
|
||||
|
||||
sanitizeStartEndSeparators() {
|
||||
if (this.documents.length > 0 && (this.documents[this.documents.length - 1] as PaperlessDocumentSeparator).is_separator) {
|
||||
this.documents.pop()
|
||||
} else if (this.documents.length > 0 && (this.documents[0] as PaperlessDocumentSeparator).is_separator) {
|
||||
this.documents.shift()
|
||||
}
|
||||
}
|
||||
|
||||
executeSplitMerge(preview: boolean, delete_source: boolean, metadata: SplitMergeMetadata): Observable<string[]> {
|
||||
this.sanitizeStartEndSeparators()
|
||||
let currentDocument = []
|
||||
let split_merge_plan = [currentDocument]
|
||||
this.documents.forEach(d => {
|
||||
if ((d as PaperlessDocumentSeparator).is_separator) {
|
||||
currentDocument = []
|
||||
split_merge_plan.push(currentDocument)
|
||||
} else {
|
||||
currentDocument.push({document: d.id, pages: (d as PaperlessDocumentPart).pages?.join(',')})
|
||||
}
|
||||
})
|
||||
|
||||
let request: SplitMergeRequest = {
|
||||
delete_source: delete_source,
|
||||
preview: preview,
|
||||
metadata: metadata,
|
||||
split_merge_plan: [
|
||||
this.documents.map(d => { return {document: d.id} })
|
||||
]
|
||||
split_merge_plan: split_merge_plan
|
||||
}
|
||||
return this.http.post<string[]>(`${environment.apiBaseUrl}split_merge/`, request)
|
||||
return this.http.post<string[]>(`${environment.apiBaseUrl}split_merge/`, request).pipe(
|
||||
catchError(this.handleError)
|
||||
)
|
||||
}
|
||||
|
||||
handleError(error: HttpErrorResponse) {
|
||||
console.log('API split_merge result error:', error)
|
||||
return throwError(error.message)
|
||||
}
|
||||
|
||||
getPreviewUrl(previewKey: string) {
|
||||
|
||||
+10
-1
@@ -4,4 +4,13 @@ $primaryFaded: #d1ddd2;
|
||||
|
||||
$theme-colors: (
|
||||
"primary": $primary
|
||||
);
|
||||
);
|
||||
|
||||
.bg-body {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
font-size: 80%;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -37,6 +37,10 @@ $border-color-dark-mode: #47494f;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-body {
|
||||
background-color: $bg-dark-mode !important;
|
||||
}
|
||||
|
||||
.text-light {
|
||||
color: $text-color-dark-mode !important;
|
||||
}
|
||||
@@ -90,6 +94,23 @@ $border-color-dark-mode: #47494f;
|
||||
color: $text-color-dark-mode !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link.bg-light {
|
||||
border-bottom-color: $border-color-dark-mode !important;
|
||||
|
||||
&:not(.active) {
|
||||
background-color: $bg-dark-mode !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $bg-dark-mode !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $bg-light-dark-mode !important;
|
||||
border-bottom-color: $bg-light-dark-mode !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@@ -420,6 +441,10 @@ $border-color-dark-mode: #47494f;
|
||||
.ngb-dp-month {
|
||||
background-color: $bg-light-dark-mode;
|
||||
}
|
||||
|
||||
app-split-merge .separator {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
body.color-scheme-dark {
|
||||
|
||||
Reference in new issue
Block a user