mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: Dynamic document storage pathes (#916)
* Added devcontainer * Add feature storage pathes * Exclude tests and add versioning * Check escaping * Check escaping * Check quoting * Echo * Escape * Escape : * Double escape \ * Escaping * Remove if * Escape colon * Missing \ * Esacpe : * Escape all * test * Remove sed * Fix exclude * Remove SED command * Add LD_LIBRARY_PATH * Adjusted to v1.7 * Updated test-cases * Remove devcontainer * Removed internal build-file * Run pre-commit * Corrected flak8 error * Adjusted to v1.7 * Updated test-cases * Corrected flak8 error * Adjusted to new plural translations * Small adjustments due to code-review backend * Adjusted line-break * Removed PAPERLESS prefix from settings variables * Corrected style change due to search+replace * First documentation draft * Revert changes to Pipfile * Add sphinx-autobuild with keep-outdated * Revert merge error that results in wrong storage path is evaluated * Adjust styles of generated files ... * Adds additional testing to cover dynamic storage path functionality * Remove unnecessary condition * Add hint to edit storage path dialog * Correct spelling of pathes to paths * Minor documentation tweaks * Minor typo * improving wrapping of filter editor buttons with new storage path button * Update .gitignore * Fix select border radius in non input-groups * Better storage path edit hint * Add note to edit storage path dialog re document_renamer * Add note to bulk edit storage path re document_renamer * Rename FILTER_STORAGE_DIRECTORY to PATH * Fix broken filter rule parsing * Show default storage if unspecified * Remove note re storage path on bulk edit * Add basic validation of filename variables Co-authored-by: Markus Kling <markus@markus-kling.net> Co-authored-by: Trenton Holmes <holmes.trenton@gmail.com> Co-authored-by: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Co-authored-by: Quinn Casey <quinn@quinncasey.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { TagListComponent } from './components/manage/tag-list/tag-list.componen
|
||||
import { NotFoundComponent } from './components/not-found/not-found.component'
|
||||
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
|
||||
import { DirtyFormGuard } from './guards/dirty-form.guard'
|
||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
@@ -27,6 +28,7 @@ const routes: Routes = [
|
||||
{ path: 'tags', component: TagListComponent },
|
||||
{ path: 'documenttypes', component: DocumentTypeListComponent },
|
||||
{ path: 'correspondents', component: CorrespondentListComponent },
|
||||
{ path: 'storagepaths', component: StoragePathListComponent },
|
||||
{ path: 'logs', component: LogsComponent },
|
||||
{
|
||||
path: 'settings',
|
||||
|
@@ -87,6 +87,8 @@ import localeSr from '@angular/common/locales/sr'
|
||||
import localeSv from '@angular/common/locales/sv'
|
||||
import localeTr from '@angular/common/locales/tr'
|
||||
import localeZh from '@angular/common/locales/zh'
|
||||
import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component'
|
||||
import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { SettingsService } from './services/settings.service'
|
||||
|
||||
registerLocaleData(localeBe)
|
||||
@@ -125,6 +127,7 @@ function initializeApp(settings: SettingsService) {
|
||||
TagListComponent,
|
||||
DocumentTypeListComponent,
|
||||
CorrespondentListComponent,
|
||||
StoragePathListComponent,
|
||||
LogsComponent,
|
||||
SettingsComponent,
|
||||
NotFoundComponent,
|
||||
@@ -132,6 +135,7 @@ function initializeApp(settings: SettingsService) {
|
||||
ConfirmDialogComponent,
|
||||
TagEditDialogComponent,
|
||||
DocumentTypeEditDialogComponent,
|
||||
StoragePathEditDialogComponent,
|
||||
TagComponent,
|
||||
PageHeaderComponent,
|
||||
AppFrameComponent,
|
||||
|
@@ -134,6 +134,13 @@
|
||||
</svg> <ng-container i18n>Document types</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="storagepaths" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
||||
</svg> <ng-container i18n>Storage paths</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()">
|
||||
<svg class="sidebaricon" fill="currentColor">
|
||||
|
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p *ngIf="messageBold"><b>{{messageBold}}</b></p>
|
||||
<p *ngIf="message">{{message}}</p>
|
||||
<p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
|
||||
|
@@ -0,0 +1,24 @@
|
||||
<form [formGroup]="objectForm" (ngSubmit)="save()">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<p *ngIf="this.dialogMode == 'edit'" i18n>
|
||||
<em>Note that editing a path does not apply changes to stored files until you have run the 'document_renamer' utility. See the <a target="_blank" href="https://paperless-ngx.readthedocs.io/en/latest/administration.html#utilities-renamer">documentation</a>.</em>
|
||||
</p>
|
||||
|
||||
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
|
||||
<app-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></app-input-text>
|
||||
<app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
|
||||
<app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></app-input-text>
|
||||
<app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||
</div>
|
||||
</form>
|
@@ -0,0 +1,50 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { FormControl, FormGroup } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-storage-path-edit-dialog',
|
||||
templateUrl: './storage-path-edit-dialog.component.html',
|
||||
styleUrls: ['./storage-path-edit-dialog.component.scss'],
|
||||
})
|
||||
export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> {
|
||||
constructor(
|
||||
service: StoragePathService,
|
||||
activeModal: NgbActiveModal,
|
||||
toastService: ToastService
|
||||
) {
|
||||
super(service, activeModal, toastService)
|
||||
}
|
||||
|
||||
get pathHint() {
|
||||
return (
|
||||
$localize`e.g.` +
|
||||
' <code>{created_year}-{title}</code> ' +
|
||||
$localize`or use slashes to add directories e.g.` +
|
||||
' <code>{created_year}/{correspondent}/{title}</code>. ' +
|
||||
$localize`See <a target="_blank" href="https://paperless-ngx.readthedocs.io/en/latest/advanced_usage.html#file-name-handling">documentation</a> for full list.`
|
||||
)
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
return $localize`Create new storage path`
|
||||
}
|
||||
|
||||
getEditTitle() {
|
||||
return $localize`Edit storage path`
|
||||
}
|
||||
|
||||
getForm(): FormGroup {
|
||||
return new FormGroup({
|
||||
name: new FormControl(''),
|
||||
path: new FormControl(''),
|
||||
matching_algorithm: new FormControl(1),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
})
|
||||
}
|
||||
}
|
@@ -9,7 +9,8 @@
|
||||
[items]="items"
|
||||
[addTag]="allowCreateNew && addItemRef"
|
||||
addTagText="Add item"
|
||||
i18n-addTagText="Used for both types and correspondents"
|
||||
i18n-addTagText="Used for both types, correspondents, storage paths"
|
||||
[placeholder]="placeholder"
|
||||
bindLabel="name"
|
||||
bindValue="id"
|
||||
(change)="onChange(value)"
|
||||
|
@@ -41,6 +41,9 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
@Input()
|
||||
suggestions: number[]
|
||||
|
||||
@Input()
|
||||
placeholder: string
|
||||
|
||||
@Output()
|
||||
createNew = new EventEmitter<string>()
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
||||
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
|
||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
<div class="invalid-feedback">
|
||||
{{error}}
|
||||
</div>
|
||||
|
@@ -8,4 +8,4 @@
|
||||
|
||||
.toast:not(.show) {
|
||||
display: block; // this corrects an ng-bootstrap bug that prevented animations
|
||||
}
|
||||
}
|
||||
|
@@ -73,6 +73,8 @@
|
||||
(createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents"></app-input-select>
|
||||
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
|
||||
(createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types"></app-input-select>
|
||||
<app-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true"
|
||||
(createNew)="createStoragePath($event)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default"></app-input-select>
|
||||
<app-input-tags formControlName="tags" [suggestions]="suggestions?.tags"></app-input-tags>
|
||||
|
||||
</ng-template>
|
||||
|
@@ -33,6 +33,9 @@ import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-su
|
||||
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type'
|
||||
import { normalizeDateStr } from 'src/app/utils/date'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
|
||||
@Component({
|
||||
@@ -66,6 +69,7 @@ export class DocumentDetailComponent
|
||||
|
||||
correspondents: PaperlessCorrespondent[]
|
||||
documentTypes: PaperlessDocumentType[]
|
||||
storagePaths: PaperlessStoragePath[]
|
||||
|
||||
documentForm: FormGroup = new FormGroup({
|
||||
title: new FormControl(''),
|
||||
@@ -73,6 +77,7 @@ export class DocumentDetailComponent
|
||||
created: new FormControl(),
|
||||
correspondent: new FormControl(),
|
||||
document_type: new FormControl(),
|
||||
storage_path: new FormControl(),
|
||||
archive_serial_number: new FormControl(),
|
||||
tags: new FormControl([]),
|
||||
})
|
||||
@@ -115,6 +120,7 @@ export class DocumentDetailComponent
|
||||
private documentTitlePipe: DocumentTitlePipe,
|
||||
private toastService: ToastService,
|
||||
private settings: SettingsService,
|
||||
private storagePathService: StoragePathService,
|
||||
private queryParamsService: QueryParamsService
|
||||
) {}
|
||||
|
||||
@@ -163,11 +169,17 @@ export class DocumentDetailComponent
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.correspondents = result.results))
|
||||
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.pipe(first())
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
|
||||
this.route.paramMap
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
@@ -230,6 +242,7 @@ export class DocumentDetailComponent
|
||||
created: this.ogDate.toISOString(),
|
||||
correspondent: doc.correspondent,
|
||||
document_type: doc.document_type,
|
||||
storage_path: doc.storage_path,
|
||||
archive_serial_number: doc.archive_serial_number,
|
||||
tags: [...doc.tags],
|
||||
})
|
||||
@@ -336,6 +349,27 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
createStoragePath(newName: string) {
|
||||
var modal = this.modalService.open(StoragePathEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
if (newName) modal.componentInstance.object = { name: newName }
|
||||
modal.componentInstance.success
|
||||
.pipe(
|
||||
switchMap((newStoragePath) => {
|
||||
return this.storagePathService
|
||||
.listAll()
|
||||
.pipe(map((storagePaths) => ({ newStoragePath, storagePaths })))
|
||||
})
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(({ newStoragePath, documentTypes: storagePaths }) => {
|
||||
this.storagePaths = storagePaths.results
|
||||
this.documentForm.get('storage_path').setValue(newStoragePath.id)
|
||||
})
|
||||
}
|
||||
|
||||
discard() {
|
||||
this.documentsService
|
||||
.get(this.documentId)
|
||||
|
@@ -53,6 +53,15 @@
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
(apply)="setDocumentTypes($event)">
|
||||
</app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="me-2 me-md-3" title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
(open)="openStoragePathDropdown()"
|
||||
[(selectionModel)]="storagePathsSelectionModel"
|
||||
(apply)="setStoragePaths($event)">
|
||||
</app-filterable-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
|
||||
|
@@ -22,6 +22,8 @@ import { MatchingModel } from 'src/app/data/matching-model'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
|
||||
@Component({
|
||||
@@ -33,10 +35,12 @@ export class BulkEditorComponent {
|
||||
tags: PaperlessTag[]
|
||||
correspondents: PaperlessCorrespondent[]
|
||||
documentTypes: PaperlessDocumentType[]
|
||||
storagePaths: PaperlessStoragePath[]
|
||||
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathsSelectionModel = new FilterableDropdownSelectionModel()
|
||||
awaitingDownload: boolean
|
||||
|
||||
constructor(
|
||||
@@ -48,7 +52,8 @@ export class BulkEditorComponent {
|
||||
private modalService: NgbModal,
|
||||
private openDocumentService: OpenDocumentsService,
|
||||
private settings: SettingsService,
|
||||
private toastService: ToastService
|
||||
private toastService: ToastService,
|
||||
private storagePathService: StoragePathService
|
||||
) {}
|
||||
|
||||
applyOnClose: boolean = this.settings.get(
|
||||
@@ -68,6 +73,9 @@ export class BulkEditorComponent {
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
}
|
||||
|
||||
private executeBulkOperation(modal, method: string, args) {
|
||||
@@ -145,6 +153,17 @@ export class BulkEditorComponent {
|
||||
})
|
||||
}
|
||||
|
||||
openStoragePathDropdown() {
|
||||
this.documentService
|
||||
.getSelectionData(Array.from(this.list.selected))
|
||||
.subscribe((s) => {
|
||||
this.applySelectionData(
|
||||
s.selected_storage_paths,
|
||||
this.storagePathsSelectionModel
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private _localizeList(items: MatchingModel[]) {
|
||||
if (items.length == 0) {
|
||||
return ''
|
||||
@@ -299,6 +318,42 @@ export class BulkEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
setStoragePaths(changedDocumentPaths: ChangedItems) {
|
||||
if (
|
||||
changedDocumentPaths.itemsToAdd.length == 0 &&
|
||||
changedDocumentPaths.itemsToRemove.length == 0
|
||||
)
|
||||
return
|
||||
|
||||
let storagePath =
|
||||
changedDocumentPaths.itemsToAdd.length > 0
|
||||
? changedDocumentPaths.itemsToAdd[0]
|
||||
: null
|
||||
|
||||
if (this.showConfirmationDialogs) {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm storage path assignment`
|
||||
if (storagePath) {
|
||||
modal.componentInstance.message = $localize`This operation will assign the storage path "${storagePath.name}" to ${this.list.selected.size} selected document(s).`
|
||||
} else {
|
||||
modal.componentInstance.message = $localize`This operation will remove the storage path from ${this.list.selected.size} selected document(s).`
|
||||
}
|
||||
modal.componentInstance.btnClass = 'btn-warning'
|
||||
modal.componentInstance.btnCaption = $localize`Confirm`
|
||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||
this.executeBulkOperation(modal, 'set_storage_path', {
|
||||
storage_path: storagePath ? storagePath.id : null,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.executeBulkOperation(null, 'set_storage_path', {
|
||||
storage_path: storagePath ? storagePath.id : null,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
applyDelete() {
|
||||
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||
backdrop: 'static',
|
||||
|
@@ -67,6 +67,13 @@
|
||||
</svg>
|
||||
<small>{{(document.document_type$ | async)?.name}}</small>
|
||||
</button>
|
||||
<button *ngIf="document.storage_path" type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2" title="Filter by storage path"
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
<small>{{(document.storage_path$ | async)?.name}}</small>
|
||||
</button>
|
||||
<div *ngIf="document.archive_serial_number" class="list-group-item me-2 bg-light text-dark p-1 border-0">
|
||||
<svg class="metadata-icon me-2 text-muted bi bi-upc-scan" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5zM3 4.5a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7zm2 0a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5v-7zm3 0a.5.5 0 0 1 1 0v7a.5.5 0 0 1-1 0v-7z"/>
|
||||
|
@@ -52,6 +52,9 @@ export class DocumentCardLargeComponent implements OnInit {
|
||||
@Output()
|
||||
clickDocumentType = new EventEmitter<number>()
|
||||
|
||||
@Output()
|
||||
clickStoragePath = new EventEmitter<number>()
|
||||
|
||||
@Output()
|
||||
clickMoreLike = new EventEmitter()
|
||||
|
||||
|
@@ -37,6 +37,13 @@
|
||||
</svg>
|
||||
<small>{{(document.document_type$ | async)?.name}}</small>
|
||||
</button>
|
||||
<button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Filter by storage path"
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted bi bi-folder" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M0 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1v7.5a2.5 2.5 0 0 1-2.5 2.5h-9A2.5 2.5 0 0 1 1 12.5V5a1 1 0 0 1-1-1V2zm2 3v7.5A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5V5H2zm13-3H1v2h14V2zM5 7.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
<small>{{(document.storage_path$ | async)?.name}}</small>
|
||||
</button>
|
||||
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||
<ng-template #dateTooltip>
|
||||
<div class="d-flex flex-column">
|
||||
|
@@ -47,6 +47,9 @@ export class DocumentCardSmallComponent implements OnInit {
|
||||
@Output()
|
||||
clickDocumentType = new EventEmitter<number>()
|
||||
|
||||
@Output()
|
||||
clickStoragePath = new EventEmitter<number>()
|
||||
|
||||
moreTags: number = null
|
||||
|
||||
@ViewChild('popover') popover: NgbPopover
|
||||
|
@@ -107,7 +107,7 @@
|
||||
<ng-template #documentListNoError>
|
||||
|
||||
<div *ngIf="displayMode == 'largeCards'">
|
||||
<app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
||||
<app-document-card-large [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" *ngFor="let d of list.documents; trackBy: trackByDocumentId" [document]="d" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)" (clickStoragePath)="clickStoragePath($event)" (clickMoreLike)="clickMoreLike(d.id)">
|
||||
</app-document-card-large>
|
||||
</div>
|
||||
|
||||
@@ -138,6 +138,12 @@
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Document type</th>
|
||||
<th class="d-none d-xl-table-cell"
|
||||
sortable="storage_path__name"
|
||||
[currentSortField]="list.sortField"
|
||||
[currentSortReverse]="list.sortReverse"
|
||||
(sort)="onSort($event)"
|
||||
i18n>Storage path</th>
|
||||
<th
|
||||
sortable="created"
|
||||
[currentSortField]="list.sortField"
|
||||
@@ -176,6 +182,11 @@
|
||||
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td class="d-none d-xl-table-cell">
|
||||
<ng-container *ngIf="d.storage_path">
|
||||
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path">{{(d.storage_path$ | async)?.name}}</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
{{d.created | customDate}}
|
||||
</td>
|
||||
@@ -187,7 +198,7 @@
|
||||
</table>
|
||||
|
||||
<div class="row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
|
||||
<app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small>
|
||||
<app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickStoragePath)="clickStoragePath($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small>
|
||||
</div>
|
||||
<div *ngIf="list.documents?.length > 15" class="mt-3">
|
||||
<ng-container *ngTemplateOutlet="pagination"></ng-container>
|
||||
|
@@ -265,6 +265,13 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
})
|
||||
}
|
||||
|
||||
clickStoragePath(storagePathID: number) {
|
||||
this.list.selectNone()
|
||||
setTimeout(() => {
|
||||
this.filterEditor.addStoragePath(storagePathID)
|
||||
})
|
||||
}
|
||||
|
||||
clickMoreLike(documentID: number) {
|
||||
this.queryParamsService.navigateWithFilterRules([
|
||||
{ rule_type: FILTER_FULLTEXT_MORELIKE, value: documentID.toString() },
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="row">
|
||||
<div class="row flex-wrap">
|
||||
<div class="col mb-2 mb-xl-0">
|
||||
<div class="form-inline d-flex align-items-center">
|
||||
<div class="input-group input-group-sm flex-fill w-auto">
|
||||
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
|
||||
<div ngbDropdown>
|
||||
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
|
||||
<div class="dropdown-menu shadow" ngbDropdownMenu>
|
||||
@@ -18,43 +18,54 @@
|
||||
<div class="w-100 d-xl-none"></div>
|
||||
<div class="col col-xl-auto">
|
||||
<div class="d-flex flex-wrap">
|
||||
<app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
[multiple]="true"
|
||||
(open)="onTagsDropdownOpen()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(open)="onCorrespondentDropdownOpen()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
(open)="onDocumentTypeDropdownOpen()"
|
||||
(selectionModelChange)="updateRules()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-date-dropdown class="mb-2 mb-xl-0"
|
||||
title="Created" i18n-title
|
||||
(datesSet)="updateRules()"
|
||||
[(dateBefore)]="dateCreatedBefore"
|
||||
[(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
|
||||
<app-date-dropdown class="mb-2 mb-xl-0"
|
||||
[(dateBefore)]="dateAddedBefore"
|
||||
[(dateAfter)]="dateAddedAfter"
|
||||
title="Added" i18n-title
|
||||
(datesSet)="updateRules()"></app-date-dropdown>
|
||||
<div class="d-flex flex-wrap mb-2 mb-lg-0">
|
||||
<app-filterable-dropdown class="flex-fill" title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
[multiple]="true"
|
||||
(open)="onTagsDropdownOpen()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
(selectionModelChange)="updateRules()"
|
||||
(open)="onCorrespondentDropdownOpen()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
(open)="onDocumentTypeDropdownOpen()"
|
||||
(selectionModelChange)="updateRules()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
<app-filterable-dropdown class="me-2 flex-fill" title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage path" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[(selectionModel)]="storagePathSelectionModel"
|
||||
(open)="onStoragePathDropdownOpen()"
|
||||
(selectionModelChange)="updateRules()"
|
||||
[allowSelectNone]="true"></app-filterable-dropdown>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<app-date-dropdown class="mb-2 mb-xl-0"
|
||||
title="Created" i18n-title
|
||||
(datesSet)="updateRules()"
|
||||
[(dateBefore)]="dateCreatedBefore"
|
||||
[(dateAfter)]="dateCreatedAfter"></app-date-dropdown>
|
||||
<app-date-dropdown class="mb-2 mb-xl-0"
|
||||
[(dateBefore)]="dateAddedBefore"
|
||||
[(dateAfter)]="dateAddedAfter"
|
||||
title="Added" i18n-title
|
||||
(datesSet)="updateRules()"></app-date-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 d-xl-none"></div>
|
||||
<div class="col col-xl-auto">
|
||||
<button class="btn btn-link btn-sm px-0 mx-0 ms-xl-n3" [disabled]="!rulesModified" (click)="resetSelected()">
|
||||
<div class="col col-xl-auto ps-0">
|
||||
<button class="btn btn-link btn-sm px-0" [disabled]="!rulesModified" (click)="resetSelected()">
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
</svg><ng-container i18n>Reset filters</ng-container>
|
||||
|
@@ -33,6 +33,7 @@ import {
|
||||
FILTER_DOES_NOT_HAVE_TAG,
|
||||
FILTER_TITLE,
|
||||
FILTER_TITLE_CONTENT,
|
||||
FILTER_STORAGE_PATH,
|
||||
FILTER_ASN_ISNULL,
|
||||
FILTER_ASN_GT,
|
||||
FILTER_ASN_LT,
|
||||
@@ -41,6 +42,8 @@ import { FilterableDropdownSelectionModel } from '../../common/filterable-dropdo
|
||||
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
|
||||
const TEXT_FILTER_TARGET_TITLE = 'title'
|
||||
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
||||
@@ -107,7 +110,8 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
private documentTypeService: DocumentTypeService,
|
||||
private tagService: TagService,
|
||||
private correspondentService: CorrespondentService,
|
||||
private documentService: DocumentService
|
||||
private documentService: DocumentService,
|
||||
private storagePathService: StoragePathService
|
||||
) {}
|
||||
|
||||
@ViewChild('textFilterInput')
|
||||
@@ -116,6 +120,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
tags: PaperlessTag[] = []
|
||||
correspondents: PaperlessCorrespondent[] = []
|
||||
documentTypes: PaperlessDocumentType[] = []
|
||||
storagePaths: PaperlessStoragePath[] = []
|
||||
|
||||
_textFilter = ''
|
||||
_moreLikeId: number
|
||||
@@ -186,6 +191,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
tagSelectionModel = new FilterableDropdownSelectionModel()
|
||||
correspondentSelectionModel = new FilterableDropdownSelectionModel()
|
||||
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
|
||||
storagePathSelectionModel = new FilterableDropdownSelectionModel()
|
||||
|
||||
dateCreatedBefore: string
|
||||
dateCreatedAfter: string
|
||||
@@ -210,6 +216,7 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this._filterRules = value
|
||||
|
||||
this.documentTypeSelectionModel.clear(false)
|
||||
this.storagePathSelectionModel.clear(false)
|
||||
this.tagSelectionModel.clear(false)
|
||||
this.correspondentSelectionModel.clear(false)
|
||||
this._textFilter = null
|
||||
@@ -297,6 +304,13 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_STORAGE_PATH:
|
||||
this.storagePathSelectionModel.set(
|
||||
rule.value ? +rule.value : null,
|
||||
ToggleableItemState.Selected,
|
||||
false
|
||||
)
|
||||
break
|
||||
case FILTER_ASN_ISNULL:
|
||||
this.textFilterTarget = TEXT_FILTER_TARGET_ASN
|
||||
this.textFilterModifier = TEXT_FILTER_MODIFIER_NULL
|
||||
@@ -418,6 +432,12 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
value: documentType.id?.toString(),
|
||||
})
|
||||
})
|
||||
this.storagePathSelectionModel.getSelectedItems().forEach((storagePath) => {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: storagePath.id?.toString(),
|
||||
})
|
||||
})
|
||||
if (this.dateCreatedBefore) {
|
||||
filterRules.push({
|
||||
rule_type: FILTER_CREATED_BEFORE,
|
||||
@@ -500,6 +520,9 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this.documentTypeService
|
||||
.listAll()
|
||||
.subscribe((result) => (this.documentTypes = result.results))
|
||||
this.storagePathService
|
||||
.listAll()
|
||||
.subscribe((result) => (this.storagePaths = result.results))
|
||||
|
||||
this.textFilterDebounce = new Subject<string>()
|
||||
|
||||
@@ -542,6 +565,13 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
}
|
||||
|
||||
addStoragePath(storagePathID: number) {
|
||||
this.storagePathSelectionModel.set(
|
||||
storagePathID,
|
||||
ToggleableItemState.Selected
|
||||
)
|
||||
}
|
||||
|
||||
onTagsDropdownOpen() {
|
||||
this.tagSelectionModel.apply()
|
||||
}
|
||||
@@ -554,6 +584,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
|
||||
this.documentTypeSelectionModel.apply()
|
||||
}
|
||||
|
||||
onStoragePathDropdownOpen() {
|
||||
this.storagePathSelectionModel.apply()
|
||||
}
|
||||
|
||||
updateTextFilter(text) {
|
||||
this._textFilter = text
|
||||
this.documentService.searchQuery = text
|
||||
|
@@ -0,0 +1,48 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { FILTER_STORAGE_PATH } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { QueryParamsService } from 'src/app/services/query-params.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { ManagementListComponent } from '../management-list/management-list.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-storage-path-list',
|
||||
templateUrl: './../management-list/management-list.component.html',
|
||||
styleUrls: ['./../management-list/management-list.component.scss'],
|
||||
})
|
||||
export class StoragePathListComponent extends ManagementListComponent<PaperlessStoragePath> {
|
||||
constructor(
|
||||
directoryService: StoragePathService,
|
||||
modalService: NgbModal,
|
||||
toastService: ToastService,
|
||||
queryParamsService: QueryParamsService
|
||||
) {
|
||||
super(
|
||||
directoryService,
|
||||
modalService,
|
||||
StoragePathEditDialogComponent,
|
||||
toastService,
|
||||
queryParamsService,
|
||||
FILTER_STORAGE_PATH,
|
||||
$localize`storage path`,
|
||||
$localize`storage paths`,
|
||||
[
|
||||
{
|
||||
key: 'path',
|
||||
name: $localize`Path`,
|
||||
valueFn: (c: PaperlessStoragePath) => {
|
||||
return c.path
|
||||
},
|
||||
},
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
getDeleteMessage(object: PaperlessStoragePath) {
|
||||
return $localize`Do you really want to delete the storage path "${object.name}"?`
|
||||
}
|
||||
}
|
@@ -28,6 +28,8 @@ export const FILTER_TITLE_CONTENT = 21
|
||||
export const FILTER_FULLTEXT_QUERY = 22
|
||||
export const FILTER_FULLTEXT_MORELIKE = 23
|
||||
|
||||
export const FILTER_STORAGE_PATH = 30
|
||||
|
||||
export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
{
|
||||
id: FILTER_TITLE,
|
||||
@@ -56,6 +58,13 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
datatype: 'correspondent',
|
||||
multi: false,
|
||||
},
|
||||
{
|
||||
id: FILTER_STORAGE_PATH,
|
||||
filtervar: 'storage_path__id',
|
||||
isnull_filtervar: 'storage_path__isnull',
|
||||
datatype: 'storage_path',
|
||||
multi: false,
|
||||
},
|
||||
{
|
||||
id: FILTER_DOCUMENT_TYPE,
|
||||
filtervar: 'document_type__id',
|
||||
@@ -180,7 +189,6 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
|
||||
datatype: 'string',
|
||||
multi: false,
|
||||
},
|
||||
|
||||
{
|
||||
id: FILTER_FULLTEXT_MORELIKE,
|
||||
filtervar: 'more_like_id',
|
||||
|
@@ -4,4 +4,6 @@ export interface PaperlessDocumentSuggestions {
|
||||
correspondents?: number[]
|
||||
|
||||
document_types?: number[]
|
||||
|
||||
storage_paths?: number[]
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { ObjectWithId } from './object-with-id'
|
||||
import { PaperlessTag } from './paperless-tag'
|
||||
import { PaperlessDocumentType } from './paperless-document-type'
|
||||
import { Observable } from 'rxjs'
|
||||
import { PaperlessStoragePath } from './paperless-storage-path'
|
||||
|
||||
export interface SearchHit {
|
||||
score?: number
|
||||
@@ -20,6 +21,10 @@ export interface PaperlessDocument extends ObjectWithId {
|
||||
|
||||
document_type?: number
|
||||
|
||||
storage_path$?: Observable<PaperlessStoragePath>
|
||||
|
||||
storage_path?: number
|
||||
|
||||
title?: string
|
||||
|
||||
content?: string
|
||||
|
5
src-ui/src/app/data/paperless-storage-path.ts
Normal file
5
src-ui/src/app/data/paperless-storage-path.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { MatchingModel } from './matching-model'
|
||||
|
||||
export interface PaperlessStoragePath extends MatchingModel {
|
||||
path?: string
|
||||
}
|
@@ -12,6 +12,7 @@ import { DocumentTypeService } from './document-type.service'
|
||||
import { TagService } from './tag.service'
|
||||
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions'
|
||||
import { filterRulesToQueryParams } from '../query-params.service'
|
||||
import { StoragePathService } from './storage-path.service'
|
||||
|
||||
export const DOCUMENT_SORT_FIELDS = [
|
||||
{ field: 'archive_serial_number', name: $localize`ASN` },
|
||||
@@ -37,6 +38,7 @@ export interface SelectionDataItem {
|
||||
}
|
||||
|
||||
export interface SelectionData {
|
||||
selected_storage_paths: SelectionDataItem[]
|
||||
selected_correspondents: SelectionDataItem[]
|
||||
selected_tags: SelectionDataItem[]
|
||||
selected_document_types: SelectionDataItem[]
|
||||
@@ -52,7 +54,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
http: HttpClient,
|
||||
private correspondentService: CorrespondentService,
|
||||
private documentTypeService: DocumentTypeService,
|
||||
private tagService: TagService
|
||||
private tagService: TagService,
|
||||
private storagePathService: StoragePathService
|
||||
) {
|
||||
super(http, 'documents')
|
||||
}
|
||||
@@ -69,6 +72,9 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
if (doc.tags) {
|
||||
doc.tags$ = this.tagService.getCachedMany(doc.tags)
|
||||
}
|
||||
if (doc.storage_path) {
|
||||
doc.storage_path$ = this.storagePathService.getCached(doc.storage_path)
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
|
13
src-ui/src/app/services/rest/storage-path.service.ts
Normal file
13
src-ui/src/app/services/rest/storage-path.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { AbstractNameFilterService } from './abstract-name-filter-service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StoragePathService extends AbstractNameFilterService<PaperlessStoragePath> {
|
||||
constructor(http: HttpClient) {
|
||||
super(http, 'storage_paths')
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user