mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Feature: Enhanced templating for filename format (#7836)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
@@ -41,6 +41,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 { TextAreaComponent } from './components/common/input/textarea/textarea.component'
|
||||
import { SelectComponent } from './components/common/input/select/select.component'
|
||||
import { CheckComponent } from './components/common/input/check/check.component'
|
||||
import { UrlComponent } from './components/common/input/url/url.component'
|
||||
@@ -440,6 +441,7 @@ function initializeApp(settings: SettingsService) {
|
||||
DocumentCardSmallComponent,
|
||||
BulkEditorComponent,
|
||||
TextComponent,
|
||||
TextAreaComponent,
|
||||
SelectComponent,
|
||||
CheckComponent,
|
||||
UrlComponent,
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<div class="modal-body">
|
||||
|
||||
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
|
||||
<pngx-input-text i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint"></pngx-input-text>
|
||||
<pngx-input-textarea i18n-title title="Path" formControlName="path" [error]="error?.path" [hint]="pathHint" [monospace]="true"></pngx-input-textarea>
|
||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
<pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||
|
@@ -10,6 +10,7 @@ import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
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'
|
||||
@@ -27,6 +28,7 @@ describe('StoragePathEditDialogComponent', () => {
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
TextAreaComponent,
|
||||
PermissionsFormComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
|
@@ -26,9 +26,9 @@ export class StoragePathEditDialogComponent extends EditDialogComponent<StorageP
|
||||
get pathHint() {
|
||||
return (
|
||||
$localize`e.g.` +
|
||||
' <code>{created_year}-{title}</code> ' +
|
||||
' <code class="text-nowrap">{{ created_year }}-{{ title }}</code> ' +
|
||||
$localize`or use slashes to add directories e.g.` +
|
||||
' <code>{created_year}/{correspondent}/{title}</code>. ' +
|
||||
' <code class="text-nowrap">{{ created_year }}/{{ title }}</code>. ' +
|
||||
$localize`See <a target="_blank" href="https://docs.paperless-ngx.com/advanced_usage/#file-name-handling">documentation</a> for full list.`
|
||||
)
|
||||
}
|
||||
|
@@ -0,0 +1,33 @@
|
||||
<div class="mb-3" [class.pb-3]="error">
|
||||
<div class="row">
|
||||
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||
@if (title) {
|
||||
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||
}
|
||||
@if (removable) {
|
||||
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||
<i-bs name="x"></i-bs> <ng-container i18n>Remove</ng-container>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="position-relative" [class.col-md-9]="horizontal">
|
||||
<textarea #inputField
|
||||
[id]="inputId"
|
||||
class="form-control"
|
||||
[class.is-invalid]="error"
|
||||
[class.font-monospace]="monospace"
|
||||
[(ngModel)]="value"
|
||||
(change)="onChange(value)"
|
||||
[disabled]="disabled"
|
||||
[placeholder]="placeholder"
|
||||
rows="6">
|
||||
</textarea>
|
||||
@if (hint) {
|
||||
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||
}
|
||||
<div class="invalid-feedback position-absolute top-100">
|
||||
{{error}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,31 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { TextAreaComponent } from './textarea.component'
|
||||
|
||||
describe('TextComponent', () => {
|
||||
let component: TextAreaComponent
|
||||
let fixture: ComponentFixture<TextAreaComponent>
|
||||
let input: HTMLTextAreaElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TextAreaComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TextAreaComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
})
|
||||
})
|
@@ -0,0 +1,27 @@
|
||||
import { Component, Input, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => TextAreaComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
selector: 'pngx-input-textarea',
|
||||
templateUrl: './textarea.component.html',
|
||||
styleUrls: ['./textarea.component.scss'],
|
||||
})
|
||||
export class TextAreaComponent extends AbstractInputComponent<string> {
|
||||
@Input()
|
||||
placeholder: string = ''
|
||||
|
||||
@Input()
|
||||
monospace: boolean = false
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
}
|
@@ -38,7 +38,7 @@
|
||||
<th scope="col" class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||
<th scope="col" class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||
@for (column of extraColumns; track column) {
|
||||
<th scope="col" class="fw-normal" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||
<th scope="col" class="fw-normal" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||
}
|
||||
<th scope="col" class="fw-normal" i18n>Actions</th>
|
||||
</tr>
|
||||
@@ -64,7 +64,7 @@
|
||||
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
||||
<td scope="row">{{ object.document_count }}</td>
|
||||
@for (column of extraColumns; track column) {
|
||||
<td scope="row">
|
||||
<td scope="row" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }">
|
||||
@if (column.rendersHtml) {
|
||||
<div [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div>
|
||||
} @else {
|
||||
|
@@ -44,6 +44,8 @@ export interface ManagementListColumn {
|
||||
valueFn: any
|
||||
|
||||
rendersHtml?: boolean
|
||||
|
||||
hideOnMobile?: boolean
|
||||
}
|
||||
|
||||
@Directive()
|
||||
|
@@ -11,6 +11,8 @@ import { PageHeaderComponent } from '../../common/page-header/page-header.compon
|
||||
import { StoragePathListComponent } from './storage-path-list.component'
|
||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { StoragePath } from 'src/app/data/storage-path'
|
||||
|
||||
describe('StoragePathListComponent', () => {
|
||||
let component: StoragePathListComponent
|
||||
@@ -24,6 +26,7 @@ describe('StoragePathListComponent', () => {
|
||||
SortableDirective,
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
imports: [
|
||||
NgbPaginationModule,
|
||||
@@ -71,4 +74,15 @@ describe('StoragePathListComponent', () => {
|
||||
'Do you really want to delete the storage path "StoragePath1"?'
|
||||
)
|
||||
})
|
||||
|
||||
it('should truncate path if necessary', () => {
|
||||
const path: StoragePath = {
|
||||
id: 1,
|
||||
name: 'StoragePath1',
|
||||
path: 'a'.repeat(100),
|
||||
}
|
||||
expect(component.extraColumns[0].valueFn(path)).toEqual(
|
||||
`<code>${'a'.repeat(49)}...</code>`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@@ -40,8 +40,10 @@ export class StoragePathListComponent extends ManagementListComponent<StoragePat
|
||||
{
|
||||
key: 'path',
|
||||
name: $localize`Path`,
|
||||
rendersHtml: true,
|
||||
hideOnMobile: true,
|
||||
valueFn: (c: StoragePath) => {
|
||||
return c.path
|
||||
return `<code>${c.path?.slice(0, 49)}${c.path?.length > 50 ? '...' : ''}</code>`
|
||||
},
|
||||
},
|
||||
]
|
||||
|
Reference in New Issue
Block a user