From 82b5d118a5a0c94f9734b28514f8b9c6cf5ccf68 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:35:06 -0700 Subject: [PATCH] Feature: live preview of storage path --- src-ui/messages.xlf | 428 +++++++----------- .../storage-path-edit-dialog.component.html | 50 ++ .../storage-path-edit-dialog.component.scss | 4 + ...storage-path-edit-dialog.component.spec.ts | 96 +++- .../storage-path-edit-dialog.component.ts | 117 ++++- .../input/textarea/textarea.component.html | 2 +- .../rest/storage-path.service.spec.ts | 28 ++ .../app/services/rest/storage-path.service.ts | 8 + src/documents/serialisers.py | 15 + src/documents/tests/test_api_objects.py | 29 ++ src/documents/views.py | 21 + src/paperless/urls.py | 6 + 12 files changed, 518 insertions(+), 286 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 7838c63ed..d6e5d2ebd 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -568,7 +568,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 29 + 79 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -700,9 +700,13 @@ src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html 35 + + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 50 + src/app/components/common/input/document-link/document-link.component.html - 51 + 38 src/app/components/common/permissions-dialog/permissions-dialog.component.html @@ -1035,7 +1039,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 152 + 143 @@ -1046,7 +1050,7 @@ src/app/components/document-list/document-list.component.html - 212 + 211 src/app/data/document.ts @@ -1092,7 +1096,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.html - 105 + 110 src/app/components/manage/mail/mail.component.html @@ -1680,7 +1684,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 28 + 78 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -1958,7 +1962,7 @@ src/app/components/document-list/document-list.component.html - 239 + 238 src/app/data/document.ts @@ -2764,7 +2768,7 @@ src/app/components/document-list/document-list.component.html - 194 + 193 src/app/components/document-list/filter-editor/filter-editor.component.html @@ -3312,102 +3316,6 @@ 70 - - True - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 40 - - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 73 - - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 79 - - - - False - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 41 - - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 74 - - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 80 - - - - Search docs... - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 96 - - - - Any - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 126 - - - src/app/components/common/filterable-dropdown/filterable-dropdown.component.html - 17 - - - - All - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 128 - - - src/app/components/common/filterable-dropdown/filterable-dropdown.component.html - 15 - - - src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html - 16 - - - src/app/components/common/permissions-select/permissions-select.component.html - 16 - - - src/app/components/common/permissions-select/permissions-select.component.html - 27 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.html - 14 - - - - Not - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 131 - - - - Add query - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 150 - - - - Add expression - - src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html - 153 - - now @@ -3449,7 +3357,7 @@ src/app/components/document-list/document-list.component.html - 248 + 247 src/app/data/document.ts @@ -3500,7 +3408,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 14 + 64 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -3519,7 +3427,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 16 + 66 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -3538,7 +3446,7 @@ src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html - 19 + 69 src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html @@ -4126,39 +4034,68 @@ 42 - - e.g. + + Preview - src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts - 28 + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 18 + + + src/app/components/document-detail/document-detail.component.html + 282 - - or use slashes to add directories e.g. + + Path test failed - src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html 30 - - See <a target="_blank" href="https://docs.paperless-ngx.com/advanced_usage/#file-name-handling">documentation</a> for full list. + + No document selected + + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 32 + + + + Search for documents + + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 38 + + + + No documents found + + src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html + 39 + + + src/app/components/common/input/document-link/document-link.component.ts + 44 + + + + See <a target="_blank" href="https://docs.paperless-ngx.com/advanced_usage/#file-name-handling">the documentation</a>. src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts - 32 + 63 Create new storage path src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts - 37 + 67 Edit storage path src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts - 41 + 71 @@ -4657,6 +4594,36 @@ 146 + + All + + src/app/components/common/filterable-dropdown/filterable-dropdown.component.html + 15 + + + src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.html + 16 + + + src/app/components/common/permissions-select/permissions-select.component.html + 16 + + + src/app/components/common/permissions-select/permissions-select.component.html + 27 + + + src/app/components/document-list/bulk-editor/bulk-editor.component.html + 14 + + + + Any + + src/app/components/common/filterable-dropdown/filterable-dropdown.component.html + 17 + + Include @@ -4746,7 +4713,7 @@ src/app/components/common/input/document-link/document-link.component.html - 12 + 9 src/app/components/common/input/file/file.component.html @@ -4822,34 +4789,20 @@ Remove link src/app/components/common/input/document-link/document-link.component.html - 43 + 30 Open link src/app/components/common/input/document-link/document-link.component.html - 44 + 31 src/app/components/common/input/url/url.component.html 14 - - No documents found - - src/app/components/common/input/document-link/document-link.component.ts - 44 - - - - Search for documents - - src/app/components/common/input/document-link/document-link.component.ts - 53 - - Selected items @@ -5599,7 +5552,7 @@ src/app/components/document-list/document-list.component.html - 288 + 286 @@ -5614,7 +5567,7 @@ src/app/components/document-list/document-list.component.html - 323 + 321 @@ -5629,7 +5582,7 @@ src/app/components/document-list/document-list.component.html - 330 + 328 @@ -5919,11 +5872,11 @@ src/app/components/document-list/document-list.component.html - 191 + 190 src/app/components/document-list/filter-editor/filter-editor.component.ts - 140 + 131 src/app/data/document.ts @@ -5960,7 +5913,7 @@ src/app/components/document-list/document-list.component.html - 181 + 180 src/app/components/document-list/filter-editor/filter-editor.component.html @@ -5987,7 +5940,7 @@ src/app/components/document-list/document-list.component.html - 221 + 220 src/app/components/document-list/filter-editor/filter-editor.component.html @@ -6014,7 +5967,7 @@ src/app/components/document-list/document-list.component.html - 230 + 229 src/app/components/document-list/filter-editor/filter-editor.component.html @@ -6120,13 +6073,6 @@ 275 - - Preview - - src/app/components/document-detail/document-detail.component.html - 282 - - Notes @@ -6505,7 +6451,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.ts - 148 + 139 @@ -6514,6 +6460,10 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.html 83 + + src/app/components/document-list/filter-editor/filter-editor.component.html + 90 + Merge @@ -6811,7 +6761,7 @@ src/app/components/document-list/document-list.component.html - 299 + 297 @@ -7010,7 +6960,7 @@ src/app/components/document-list/filter-editor/filter-editor.component.html - 111 + 116 @@ -7024,18 +6974,18 @@ Sort by ASN src/app/components/document-list/document-list.component.html - 168 + 167 ASN src/app/components/document-list/document-list.component.html - 172 + 171 src/app/components/document-list/filter-editor/filter-editor.component.ts - 145 + 136 src/app/data/document.ts @@ -7050,28 +7000,28 @@ Sort by correspondent src/app/components/document-list/document-list.component.html - 177 + 176 Sort by title src/app/components/document-list/document-list.component.html - 186 + 185 Sort by owner src/app/components/document-list/document-list.component.html - 199 + 198 Owner src/app/components/document-list/document-list.component.html - 203 + 202 src/app/data/document.ts @@ -7086,49 +7036,49 @@ Sort by notes src/app/components/document-list/document-list.component.html - 208 + 207 Sort by document type src/app/components/document-list/document-list.component.html - 217 + 216 Sort by storage path src/app/components/document-list/document-list.component.html - 226 + 225 Sort by created date src/app/components/document-list/document-list.component.html - 235 + 234 Sort by added date src/app/components/document-list/document-list.component.html - 244 + 243 Sort by number of pages src/app/components/document-list/document-list.component.html - 253 + 252 Pages src/app/components/document-list/document-list.component.html - 257 + 256 src/app/data/document.ts @@ -7147,21 +7097,21 @@ Shared src/app/components/document-list/document-list.component.html - 260,262 + 259,261 Edit document src/app/components/document-list/document-list.component.html - 295 + 293 Yes src/app/components/document-list/document-list.component.html - 351 + 349 src/app/pipes/yes-no.pipe.ts @@ -7172,7 +7122,7 @@ No src/app/components/document-list/document-list.component.html - 351 + 349 src/app/pipes/yes-no.pipe.ts @@ -7211,154 +7161,161 @@ Dates src/app/components/document-list/filter-editor/filter-editor.component.html - 95 + 100 Title & content src/app/components/document-list/filter-editor/filter-editor.component.ts - 143 + 134 More like src/app/components/document-list/filter-editor/filter-editor.component.ts - 158 + 149 equals src/app/components/document-list/filter-editor/filter-editor.component.ts - 164 + 155 is empty src/app/components/document-list/filter-editor/filter-editor.component.ts - 168 + 159 is not empty src/app/components/document-list/filter-editor/filter-editor.component.ts - 172 + 163 greater than src/app/components/document-list/filter-editor/filter-editor.component.ts - 176 + 167 less than src/app/components/document-list/filter-editor/filter-editor.component.ts - 180 + 171 Correspondent: src/app/components/document-list/filter-editor/filter-editor.component.ts - 200,202 + 191,193 Without correspondent src/app/components/document-list/filter-editor/filter-editor.component.ts - 204 + 195 Document type: src/app/components/document-list/filter-editor/filter-editor.component.ts - 210,212 + 201,203 Without document type src/app/components/document-list/filter-editor/filter-editor.component.ts - 214 + 205 Storage path: src/app/components/document-list/filter-editor/filter-editor.component.ts - 220,222 + 211,213 Without storage path src/app/components/document-list/filter-editor/filter-editor.component.ts - 224 + 215 Tag: src/app/components/document-list/filter-editor/filter-editor.component.ts - 228,230 + 219,221 Without any tag src/app/components/document-list/filter-editor/filter-editor.component.ts - 234 + 225 - - Custom fields query + + Custom fields: src/app/components/document-list/filter-editor/filter-editor.component.ts - 238 + 229,231 + + + + Without any custom field + + src/app/components/document-list/filter-editor/filter-editor.component.ts + 235 Title: src/app/components/document-list/filter-editor/filter-editor.component.ts - 241 + 239 ASN: src/app/components/document-list/filter-editor/filter-editor.component.ts - 244 + 242 Owner: src/app/components/document-list/filter-editor/filter-editor.component.ts - 247 + 245 Owner not in: src/app/components/document-list/filter-editor/filter-editor.component.ts - 250 + 248 Without an owner src/app/components/document-list/filter-editor/filter-editor.component.ts - 253 + 251 @@ -8089,83 +8046,6 @@ 9 - - Equal to - - src/app/data/custom-field-query.ts - 24 - - - - In - - src/app/data/custom-field-query.ts - 25 - - - - Is null - - src/app/data/custom-field-query.ts - 26 - - - - Exists - - src/app/data/custom-field-query.ts - 27 - - - - Contains - - src/app/data/custom-field-query.ts - 28 - - - - Contains (case-insensitive) - - src/app/data/custom-field-query.ts - 29 - - - - Greater than - - src/app/data/custom-field-query.ts - 30 - - - - Greater than or equal to - - src/app/data/custom-field-query.ts - 31 - - - - Less than - - src/app/data/custom-field-query.ts - 32 - - - - Less than or equal to - - src/app/data/custom-field-query.ts - 33 - - - - Range - - src/app/data/custom-field-query.ts - 34 - - Boolean diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html index f8232f957..06b449990 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html @@ -11,6 +11,56 @@ + +
+
+

+ +

+
+
+ +
+
+ @if (testLoading) { + + } @else if (testResult) { + {{testResult}} + } @else if (testFailed) { +
Path test failed
+ } @else { +
No document selected
+ } +
+
+ + +
+
Loading...
+
+ +
{{document.title}} ({{document.created | customDate:'shortDate'}})
+
+
+
+
+
+
+
+ +
+ @if (patternRequired) { diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.scss b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.scss index e69de29bb..3e16b3d52 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.scss +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.scss @@ -0,0 +1,4 @@ +.accordion { + --bs-accordion-btn-padding-x: 0.75rem; + --bs-accordion-btn-padding-y: 0.375rem; + } diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.spec.ts index 051d21527..174397981 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.spec.ts +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.spec.ts @@ -1,7 +1,11 @@ import { provideHttpClientTesting } from '@angular/common/http/testing' import { ComponentFixture, TestBed } from '@angular/core/testing' import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' +import { + NgbAccordionButton, + NgbActiveModal, + NgbModule, +} from '@ng-bootstrap/ng-bootstrap' import { NgSelectModule } from '@ng-select/ng-select' import { IfOwnerDirective } from 'src/app/directives/if-owner.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' @@ -14,10 +18,16 @@ import { TextAreaComponent } from '../../input/textarea/textarea.component' import { EditDialogMode } from '../edit-dialog.component' import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component' import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' +import { StoragePathService } from 'src/app/services/rest/storage-path.service' +import { DocumentService } from 'src/app/services/rest/document.service' +import { of, throwError } from 'rxjs' +import { FILTER_TITLE } from 'src/app/data/filter-rule-type' +import { By } from '@angular/platform-browser' describe('StoragePathEditDialogComponent', () => { let component: StoragePathEditDialogComponent let settingsService: SettingsService + let documentService: DocumentService let fixture: ComponentFixture beforeEach(async () => { @@ -40,6 +50,7 @@ describe('StoragePathEditDialogComponent', () => { ], }).compileComponents() + documentService = TestBed.inject(DocumentService) fixture = TestBed.createComponent(StoragePathEditDialogComponent) settingsService = TestBed.inject(SettingsService) settingsService.currentUser = { id: 99, username: 'user99' } @@ -59,4 +70,87 @@ describe('StoragePathEditDialogComponent', () => { fixture.detectChanges() expect(editTitleSpy).toHaveBeenCalled() }) + + it('should support test path', () => { + const testSpy = jest.spyOn( + component['service'] as StoragePathService, + 'testPath' + ) + testSpy.mockReturnValueOnce(of('test/abc123')) + component.objectForm.patchValue({ path: 'test/{{title}}' }) + fixture.detectChanges() + component.testPath({ id: 1 }) + expect(testSpy).toHaveBeenCalledWith('test/{{title}}', 1) + expect(component.testResult).toBe('test/abc123') + expect(component.testFailed).toBeFalsy() + + // test failed + testSpy.mockReturnValueOnce(of('')) + component.testPath({ id: 1 }) + expect(component.testResult).toBeNull() + expect(component.testFailed).toBeTruthy() + + component.testPath(null) + expect(component.testResult).toBeNull() + }) + + it('should compare two documents by id', () => { + const doc1 = { id: 1 } + const doc2 = { id: 2 } + expect(component.compareDocuments(doc1, doc1)).toBeTruthy() + expect(component.compareDocuments(doc1, doc2)).toBeFalsy() + }) + + it('should use id as trackBy', () => { + expect(component.trackByFn({ id: 1 })).toBe(1) + }) + + it('should search on select text input', () => { + fixture.debugElement + .query(By.directive(NgbAccordionButton)) + .triggerEventHandler('click', null) + fixture.detectChanges() + const documents = [ + { id: 1, title: 'foo' }, + { id: 2, title: 'bar' }, + ] + const listSpy = jest.spyOn(documentService, 'listFiltered') + listSpy.mockReturnValueOnce( + of({ + count: 1, + results: documents[0], + all: [1], + } as any) + ) + component.documentsInput$.next('bar') + expect(listSpy).toHaveBeenCalledWith( + 1, + null, + 'created', + true, + [{ rule_type: FILTER_TITLE, value: 'bar' }], + { truncate_content: true } + ) + listSpy.mockReturnValueOnce( + of({ + count: 2, + results: [...documents], + all: [1, 2], + } as any) + ) + component.documentsInput$.next('ba') + listSpy.mockReturnValueOnce(throwError(() => new Error())) + component.documentsInput$.next('foo') + }) + + it('should run path test on path change', () => { + const testSpy = jest.spyOn(component, 'testPath') + component['testDocument'] = { id: 1 } as any + component.objectForm.patchValue( + { path: 'test/{{title}}' }, + { emitEvent: true } + ) + fixture.detectChanges() + expect(testSpy).toHaveBeenCalled() + }) }) diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts index 0f9cc9711..67a5e3019 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts @@ -1,9 +1,25 @@ -import { Component } from '@angular/core' +import { Component, OnDestroy } from '@angular/core' import { FormControl, FormGroup } from '@angular/forms' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { + Subject, + Observable, + concat, + of, + distinctUntilChanged, + takeUntil, + tap, + switchMap, + map, + catchError, + filter, +} from 'rxjs' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' +import { Document } from 'src/app/data/document' +import { FILTER_TITLE } from 'src/app/data/filter-rule-type' import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' import { StoragePath } from 'src/app/data/storage-path' +import { DocumentService } from 'src/app/services/rest/document.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' @@ -13,24 +29,38 @@ import { SettingsService } from 'src/app/services/settings.service' templateUrl: './storage-path-edit-dialog.component.html', styleUrls: ['./storage-path-edit-dialog.component.scss'], }) -export class StoragePathEditDialogComponent extends EditDialogComponent { +export class StoragePathEditDialogComponent + extends EditDialogComponent + implements OnDestroy +{ + public documentsInput$ = new Subject() + public foundDocuments$: Observable + private testDocument: Document + public testResult: string + public testFailed: boolean = false + public loading = false + public testLoading = false + + private unsubscribeNotifier: Subject = new Subject() + constructor( service: StoragePathService, activeModal: NgbActiveModal, userService: UserService, - settingsService: SettingsService + settingsService: SettingsService, + private documentsService: DocumentService ) { super(service, activeModal, userService, settingsService) + this.initPathObservables() + } + + ngOnDestroy(): void { + this.unsubscribeNotifier.next(this) + this.unsubscribeNotifier.complete() } get pathHint() { - return ( - $localize`e.g.` + - ' {{ created_year }}-{{ title }} ' + - $localize`or use slashes to add directories e.g.` + - ' {{ created_year }}/{{ title }}. ' + - $localize`See documentation for full list.` - ) + return $localize`See the documentation.` } getCreateTitle() { @@ -51,4 +81,71 @@ export class StoragePathEditDialogComponent extends EditDialogComponent { + if (result?.length) { + this.testResult = result + this.testFailed = false + } else { + this.testResult = null + this.testFailed = true + } + this.testLoading = false + }) + } + + compareDocuments(document: Document, selectedDocument: Document) { + return document.id === selectedDocument.id + } + + private initPathObservables() { + this.objectForm + .get('path') + .valueChanges.pipe( + takeUntil(this.unsubscribeNotifier), + filter((path) => path && !!this.testDocument) + ) + .subscribe(() => { + this.testPath(this.testDocument) + }) + + this.foundDocuments$ = concat( + of([]), // default items + this.documentsInput$.pipe( + tap(() => console.log('searching')), + distinctUntilChanged(), + takeUntil(this.unsubscribeNotifier), + tap(() => (this.loading = true)), + switchMap((title) => + this.documentsService + .listFiltered( + 1, + null, + 'created', + true, + [{ rule_type: FILTER_TITLE, value: title }], + { truncate_content: true } + ) + .pipe( + map((result) => result.results), + catchError(() => of([])), // empty on error + tap(() => (this.loading = false)) + ) + ) + ) + ) + } + + trackByFn(item: Document) { + return item.id + } } diff --git a/src-ui/src/app/components/common/input/textarea/textarea.component.html b/src-ui/src/app/components/common/input/textarea/textarea.component.html index b92bef476..d92a8aa4f 100644 --- a/src-ui/src/app/components/common/input/textarea/textarea.component.html +++ b/src-ui/src/app/components/common/input/textarea/textarea.component.html @@ -20,7 +20,7 @@ (change)="onChange(value)" [disabled]="disabled" [placeholder]="placeholder" - rows="6"> + rows="4"> @if (hint) { diff --git a/src-ui/src/app/services/rest/storage-path.service.spec.ts b/src-ui/src/app/services/rest/storage-path.service.spec.ts index f365f6aa1..8b67a125b 100644 --- a/src-ui/src/app/services/rest/storage-path.service.spec.ts +++ b/src-ui/src/app/services/rest/storage-path.service.spec.ts @@ -1,7 +1,35 @@ import { StoragePathService } from './storage-path.service' import { commonAbstractNameFilterPaperlessServiceTests } from './abstract-name-filter-service.spec' +import { Subscription } from 'rxjs' +import { HttpTestingController } from '@angular/common/http/testing' +import { TestBed } from '@angular/core/testing' +import { environment } from 'src/environments/environment' + +let httpTestingController: HttpTestingController +let service: StoragePathService +let subscription: Subscription +const endpoint = 'storage_paths' commonAbstractNameFilterPaperlessServiceTests( 'storage_paths', StoragePathService ) + +describe(`Additional service tests for StoragePathservice`, () => { + beforeEach(() => { + httpTestingController = TestBed.inject(HttpTestingController) + service = TestBed.inject(StoragePathService) + }) + + afterEach(() => { + subscription?.unsubscribe() + httpTestingController.verify() + }) + + it('should support testing path', () => { + subscription = service.testPath('path', 11).subscribe() + httpTestingController + .expectOne(`${environment.apiBaseUrl}${endpoint}/test/`) + .flush('ok') + }) +}) diff --git a/src-ui/src/app/services/rest/storage-path.service.ts b/src-ui/src/app/services/rest/storage-path.service.ts index 52997c7a0..1ac7c82d7 100644 --- a/src-ui/src/app/services/rest/storage-path.service.ts +++ b/src-ui/src/app/services/rest/storage-path.service.ts @@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http' import { Injectable } from '@angular/core' import { StoragePath } from 'src/app/data/storage-path' import { AbstractNameFilterService } from './abstract-name-filter-service' +import { Observable } from 'rxjs' @Injectable({ providedIn: 'root', @@ -10,4 +11,11 @@ export class StoragePathService extends AbstractNameFilterService { constructor(http: HttpClient) { super(http, 'storage_paths') } + + public testPath(path: string, documentID: number): Observable { + return this.http.post(`${this.getResourceUrl()}test/`, { + path, + document: documentID, + }) + } } diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 7c6e5a3ff..6f7dc8be0 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -2010,3 +2010,18 @@ class TrashSerializer(SerializerWithPerms): "Some documents in the list have not yet been deleted.", ) return documents + + +class StoragePathTestSerializer(SerializerWithPerms): + path = serializers.CharField( + required=True, + label="Path", + write_only=True, + ) + + document = serializers.PrimaryKeyRelatedField( + queryset=Document.objects.all(), + required=True, + label="Document", + write_only=True, + ) diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index c74248b9a..d4d3c729e 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -306,6 +306,35 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): # only called once bulk_update_mock.assert_called_once_with([document.pk]) + def test_test_storage_path(self): + """ + GIVEN: + - API request to test a storage path + WHEN: + - API is called + THEN: + - Correct HTTP response + - Correct response data + """ + document = Document.objects.create( + mime_type="application/pdf", + storage_path=self.sp1, + title="Something", + checksum="123", + ) + response = self.client.post( + f"{self.ENDPOINT}test/", + json.dumps( + { + "document": document.id, + "path": "path/{{ title }}", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, "path/Something") + class TestBulkEditObjects(APITestCase): # See test_api_permissions.py for bulk tests on permissions diff --git a/src/documents/views.py b/src/documents/views.py index 94674a83f..a3e19aba1 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -140,6 +140,7 @@ from documents.serialisers import SavedViewSerializer from documents.serialisers import SearchResultSerializer from documents.serialisers import ShareLinkSerializer from documents.serialisers import StoragePathSerializer +from documents.serialisers import StoragePathTestSerializer from documents.serialisers import TagSerializer from documents.serialisers import TagSerializerVersion1 from documents.serialisers import TasksViewSerializer @@ -151,6 +152,7 @@ from documents.serialisers import WorkflowTriggerSerializer from documents.signals import document_updated from documents.tasks import consume_file from documents.tasks import empty_trash +from documents.templating.filepath import validate_filepath_template_and_render from paperless import version from paperless.celery import app as celery_app from paperless.config import GeneralConfig @@ -1549,6 +1551,25 @@ class StoragePathViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): return response +class StoragePathTestView(GenericAPIView): + """ + Test storage path against a document + """ + + permission_classes = [IsAuthenticated] + serializer_class = StoragePathTestSerializer + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + document = serializer.validated_data.get("document") + path = serializer.validated_data.get("path") + + result = validate_filepath_template_and_render(path, document) + return Response(result) + + class UiSettingsView(GenericAPIView): queryset = UiSettings.objects.all() permission_classes = (IsAuthenticated, PaperlessObjectPermissions) diff --git a/src/paperless/urls.py b/src/paperless/urls.py index 4de9f3662..1b9ab5053 100644 --- a/src/paperless/urls.py +++ b/src/paperless/urls.py @@ -32,6 +32,7 @@ from documents.views import SelectionDataView from documents.views import SharedLinkView from documents.views import ShareLinkViewSet from documents.views import StatisticsView +from documents.views import StoragePathTestView from documents.views import StoragePathViewSet from documents.views import SystemStatusView from documents.views import TagViewSet @@ -165,6 +166,11 @@ urlpatterns = [ TrashView.as_view(), name="trash", ), + re_path( + "^storage_paths/test/", + StoragePathTestView.as_view(), + name="storage_paths_test", + ), *api_router.urls, ], ),