From 937f5e3ffae2bc84eba417694597bf32d3b147fd Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Wed, 20 Dec 2023 23:43:44 -0800
Subject: [PATCH] Very basic but dynamically-generated config form
---
.../admin/config/config.component.html | 151 +++++-------------
.../admin/config/config.component.spec.ts | 80 +++++++++-
.../admin/config/config.component.ts | 105 ++++++++----
src-ui/src/app/data/paperless-config.ts | 121 ++++++++++++++
.../src/app/services/config.service.spec.ts | 32 +++-
src-ui/src/app/services/config.service.ts | 4 +-
6 files changed, 339 insertions(+), 154 deletions(-)
diff --git a/src-ui/src/app/components/admin/config/config.component.html b/src-ui/src/app/components/admin/config/config.component.html
index 9167dfc34..fcc07ec95 100644
--- a/src-ui/src/app/components/admin/config/config.component.html
+++ b/src-ui/src/app/components/admin/config/config.component.html
@@ -1,116 +1,41 @@
-
+
+ @for (category of optionCategories; track category) {
+ -
+ {{category}}
+
+
+ @for (option of getCategoryOptions(category); track option.key) {
+
+
+
+ @switch (option.type) {
+ @case (ConfigOptionType.Select) {
}
+ @case (ConfigOptionType.Number) {
}
+ @case (ConfigOptionType.Boolean) {
}
+ @case (ConfigOptionType.String) {
}
+ }
+
+
+ }
+
+
+
+ }
+
+
+
+
diff --git a/src-ui/src/app/components/admin/config/config.component.spec.ts b/src-ui/src/app/components/admin/config/config.component.spec.ts
index e8de0237b..a625cc1b2 100644
--- a/src-ui/src/app/components/admin/config/config.component.spec.ts
+++ b/src-ui/src/app/components/admin/config/config.component.spec.ts
@@ -1,22 +1,96 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ConfigComponent } from './config.component'
+import { ConfigService } from 'src/app/services/config.service'
+import { ToastService } from 'src/app/services/toast.service'
+import { of, throwError } from 'rxjs'
+import { OutputTypeConfig } from 'src/app/data/paperless-config'
+import { HttpClientTestingModule } from '@angular/common/http/testing'
+import { BrowserModule } from '@angular/platform-browser'
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
+import { NgSelectModule } from '@ng-select/ng-select'
+import { TextComponent } from '../../common/input/text/text.component'
+import { NumberComponent } from '../../common/input/number/number.component'
+import { CheckComponent } from '../../common/input/check/check.component'
+import { FormsModule, ReactiveFormsModule } from '@angular/forms'
+import { PageHeaderComponent } from '../../common/page-header/page-header.component'
+import { SelectComponent } from '../../common/input/select/select.component'
describe('ConfigComponent', () => {
let component: ConfigComponent
let fixture: ComponentFixture
+ let configService: ConfigService
+ let toastService: ToastService
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [ConfigComponent],
+ declarations: [
+ ConfigComponent,
+ TextComponent,
+ SelectComponent,
+ NumberComponent,
+ CheckComponent,
+ PageHeaderComponent,
+ ],
+ imports: [
+ HttpClientTestingModule,
+ BrowserModule,
+ NgbModule,
+ NgSelectModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ],
}).compileComponents()
+ configService = TestBed.inject(ConfigService)
+ toastService = TestBed.inject(ToastService)
fixture = TestBed.createComponent(ConfigComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
- it('should create', () => {
- expect(component).toBeTruthy()
+ it('should load config on init, show error if necessary', () => {
+ const getSpy = jest.spyOn(configService, 'getConfig')
+ const errorSpy = jest.spyOn(toastService, 'showError')
+ getSpy.mockReturnValueOnce(
+ throwError(() => new Error('Error getting config'))
+ )
+ component.ngOnInit()
+ expect(getSpy).toHaveBeenCalled()
+ expect(errorSpy).toHaveBeenCalled()
+ getSpy.mockReturnValueOnce(
+ of({ output_type: OutputTypeConfig.PDF_A } as any)
+ )
+ component.ngOnInit()
+ expect(component.initialConfig).toEqual({
+ output_type: OutputTypeConfig.PDF_A,
+ })
+ })
+
+ it('should save config, show error if necessary', () => {
+ const saveSpy = jest.spyOn(configService, 'saveConfig')
+ const errorSpy = jest.spyOn(toastService, 'showError')
+ saveSpy.mockReturnValueOnce(
+ throwError(() => new Error('Error saving config'))
+ )
+ component.saveConfig()
+ expect(saveSpy).toHaveBeenCalled()
+ expect(errorSpy).toHaveBeenCalled()
+ saveSpy.mockReturnValueOnce(
+ of({ output_type: OutputTypeConfig.PDF_A } as any)
+ )
+ component.saveConfig()
+ expect(component.initialConfig).toEqual({
+ output_type: OutputTypeConfig.PDF_A,
+ })
+ })
+
+ it('should support discard changes', () => {
+ component.initialConfig = { output_type: OutputTypeConfig.PDF_A2 } as any
+ component.configForm.get('output_type').patchValue(OutputTypeConfig.PDF_A)
+ component.discardChanges()
+ expect(component.configForm.get('output_type').value).toEqual(
+ OutputTypeConfig.PDF_A2
+ )
})
})
diff --git a/src-ui/src/app/components/admin/config/config.component.ts b/src-ui/src/app/components/admin/config/config.component.ts
index 9dc3d5338..f9bc823c2 100644
--- a/src-ui/src/app/components/admin/config/config.component.ts
+++ b/src-ui/src/app/components/admin/config/config.component.ts
@@ -5,14 +5,14 @@ import {
Observable,
Subject,
Subscription,
+ first,
takeUntil,
} from 'rxjs'
import {
- ArchiveFileConfig,
- CleanConfig,
- ColorConvertConfig,
- ModeConfig,
- OutputTypeConfig,
+ PaperlessConfigOptions,
+ ConfigCategory,
+ ConfigOption,
+ ConfigOptionType,
PaperlessConfig,
} from 'src/app/data/paperless-config'
import { ConfigService } from 'src/app/services/config.service'
@@ -29,32 +29,36 @@ export class ConfigComponent
extends ComponentWithPermissions
implements OnInit, OnDestroy, DirtyComponent
{
- public ConfigChoices = {
- output_type: Object.values(OutputTypeConfig),
- mode: Object.values(ModeConfig),
- skip_archive_file: Object.values(ArchiveFileConfig),
- unpaper_clean: Object.values(CleanConfig),
- color_conversion_strategy: Object.values(ColorConvertConfig),
- }
+ public readonly ConfigOptionType = ConfigOptionType
public configForm = new FormGroup({
- output_type: new FormControl(null),
- pages: new FormControl(null),
- language: new FormControl(null),
- mode: new FormControl(null),
- skip_archive_file: new FormControl(null),
- image_dpi: new FormControl(null),
- unpaper_clean: new FormControl(null),
- deskew: new FormControl(null),
- rotate_pages: new FormControl(null),
- rotate_pages_threshold: new FormControl(null),
- max_image_pixels: new FormControl(null),
- color_conversion_strategy: new FormControl(null),
- user_args: new FormControl(null),
+ id: new FormControl(),
+ output_type: new FormControl(),
+ pages: new FormControl(),
+ language: new FormControl(),
+ mode: new FormControl(),
+ skip_archive_file: new FormControl(),
+ image_dpi: new FormControl(),
+ unpaper_clean: new FormControl(),
+ deskew: new FormControl(),
+ rotate_pages: new FormControl(),
+ rotate_pages_threshold: new FormControl(),
+ max_image_pixels: new FormControl(),
+ color_conversion_strategy: new FormControl(),
+ user_args: new FormControl(),
})
+ get optionCategories(): string[] {
+ return Object.values(ConfigCategory)
+ }
+
+ getCategoryOptions(category: string): ConfigOption[] {
+ return PaperlessConfigOptions.filter((o) => o.category === category)
+ }
+
public loading: boolean = false
+ initialConfig: PaperlessConfig
store: BehaviorSubject
storeSub: Subscription
isDirty$: Observable
@@ -69,14 +73,17 @@ export class ConfigComponent
}
ngOnInit(): void {
+ this.loading = true
this.configService
.getConfig()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (config) => {
+ this.loading = false
this.initialize(config)
},
error: (e) => {
+ this.loading = false
this.toastService.showError($localize`Error retrieving config`, e)
},
})
@@ -88,21 +95,51 @@ export class ConfigComponent
}
private initialize(config: PaperlessConfig) {
- this.store = new BehaviorSubject(config)
+ if (!this.store) {
+ this.store = new BehaviorSubject(config)
- this.store
- .asObservable()
- .pipe(takeUntil(this.unsubscribeNotifier))
- .subscribe((state) => {
- this.configForm.patchValue(state, { emitEvent: false })
- })
+ this.store
+ .asObservable()
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe((state) => {
+ this.configForm.patchValue(state, { emitEvent: false })
+ })
- this.isDirty$ = dirtyCheck(this.configForm, this.store.asObservable())
+ this.isDirty$ = dirtyCheck(this.configForm, this.store.asObservable())
+ }
this.configForm.patchValue(config)
+
+ this.initialConfig = config
+ }
+
+ getDocsUrl(key: string) {
+ return `https://docs.paperless-ngx.com/configuration/#${key}`
}
public saveConfig() {
- throw Error('Not Implemented')
+ this.loading = true
+ this.configService
+ .saveConfig(this.configForm.value as PaperlessConfig)
+ .pipe(takeUntil(this.unsubscribeNotifier), first())
+ .subscribe({
+ next: (config) => {
+ this.loading = false
+ this.initialize(config)
+ this.store.next(config)
+ this.toastService.showInfo($localize`Configuration updated`)
+ },
+ error: (e) => {
+ this.loading = false
+ this.toastService.showError(
+ $localize`An error occurred updating configuration`,
+ e
+ )
+ },
+ })
+ }
+
+ public discardChanges() {
+ this.configForm.reset(this.initialConfig)
}
}
diff --git a/src-ui/src/app/data/paperless-config.ts b/src-ui/src/app/data/paperless-config.ts
index bd251d05c..5720235e3 100644
--- a/src-ui/src/app/data/paperless-config.ts
+++ b/src-ui/src/app/data/paperless-config.ts
@@ -37,6 +37,127 @@ export enum ColorConvertConfig {
CMYK = 'CMYK',
}
+export enum ConfigOptionType {
+ String = 'string',
+ Number = 'number',
+ Select = 'select',
+ Boolean = 'boolean',
+}
+
+export const ConfigCategory = {
+ OCR: $localize`OCR Settings`,
+}
+
+export interface ConfigOption {
+ key: string
+ title: string
+ type: ConfigOptionType
+ choices?: Array
+ config_key?: string
+ category: string
+}
+
+function mapToFlatChoices(enumObj: Object): Array {
+ return Object.keys(enumObj).map((key) => {
+ return {
+ id: enumObj[key],
+ name: enumObj[key],
+ }
+ })
+}
+
+export const PaperlessConfigOptions: ConfigOption[] = [
+ {
+ key: 'output_type',
+ title: $localize`Output Type`,
+ type: ConfigOptionType.Select,
+ choices: mapToFlatChoices(OutputTypeConfig),
+ config_key: 'PAPERLESS_OCR_OUTPUT_TYPE',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'pages',
+ title: $localize`Pages`,
+ type: ConfigOptionType.Number,
+ config_key: 'PAPERLESS_OCR_PAGES',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'mode',
+ title: $localize`Mode`,
+ type: ConfigOptionType.Select,
+ choices: mapToFlatChoices(ModeConfig),
+ config_key: 'PAPERLESS_OCR_MODE',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'skip_archive_file',
+ title: $localize`Skip Archive File`,
+ type: ConfigOptionType.Select,
+ choices: mapToFlatChoices(ArchiveFileConfig),
+ config_key: 'PAPERLESS_OCR_SKIP_ARCHIVE_FILE',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'image_dpi',
+ title: $localize`Image DPI`,
+ type: ConfigOptionType.Number,
+ config_key: 'PAPERLESS_OCR_IMAGE_DPI',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'unpaper_clean',
+ title: $localize`Clean`,
+ type: ConfigOptionType.Select,
+ choices: mapToFlatChoices(CleanConfig),
+ config_key: 'PAPERLESS_OCR_CLEAN',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'deskew',
+ title: $localize`Deskew`,
+ type: ConfigOptionType.Boolean,
+ config_key: 'PAPERLESS_OCR_DESKEW',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'rotate_pages',
+ title: $localize`Rotate Pages`,
+ type: ConfigOptionType.Boolean,
+ config_key: 'PAPERLESS_OCR_ROTATE_PAGES',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'rotate_pages_threshold',
+ title: $localize`Rotate Pages Threshold`,
+ type: ConfigOptionType.Number,
+ config_key: 'PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'max_image_pixels',
+ title: $localize`Max Image Pixels`,
+ type: ConfigOptionType.Number,
+ config_key: 'PAPERLESS_OCR_IMAGE_DPI',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'color_conversion_strategy',
+ title: $localize`Color Conversion Strategy`,
+ type: ConfigOptionType.Select,
+ choices: mapToFlatChoices(ColorConvertConfig),
+ config_key: 'PAPERLESS_OCR_COLOR_CONVERSION_STRATEGY',
+ category: ConfigCategory.OCR,
+ },
+ {
+ key: 'user_args',
+ title: $localize`OCR Arguments`,
+ type: ConfigOptionType.String,
+ config_key: 'PAPERLESS_OCR_USER_ARGS',
+ category: ConfigCategory.OCR,
+ },
+]
+
export interface PaperlessConfig extends ObjectWithId {
output_type: OutputTypeConfig
pages: number
diff --git a/src-ui/src/app/services/config.service.spec.ts b/src-ui/src/app/services/config.service.spec.ts
index 0c1357ff6..3cfadb051 100644
--- a/src-ui/src/app/services/config.service.spec.ts
+++ b/src-ui/src/app/services/config.service.spec.ts
@@ -1,16 +1,42 @@
import { TestBed } from '@angular/core/testing'
import { ConfigService } from './config.service'
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing'
+import { environment } from 'src/environments/environment'
+import { OutputTypeConfig, PaperlessConfig } from '../data/paperless-config'
describe('ConfigService', () => {
let service: ConfigService
+ let httpTestingController: HttpTestingController
beforeEach(() => {
- TestBed.configureTestingModule({})
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ })
service = TestBed.inject(ConfigService)
+ httpTestingController = TestBed.inject(HttpTestingController)
})
- it('should be created', () => {
- expect(service).toBeTruthy()
+ it('should call correct API endpoint on get config', () => {
+ service.getConfig().subscribe()
+ httpTestingController
+ .expectOne(`${environment.apiBaseUrl}config/`)
+ .flush([{}])
+ })
+
+ it('should call correct API endpoint on set config', () => {
+ service
+ .saveConfig({
+ id: 1,
+ output_type: OutputTypeConfig.PDF_A,
+ } as PaperlessConfig)
+ .subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}config/1/`
+ )
+ expect(req.request.method).toEqual('PATCH')
})
})
diff --git a/src-ui/src/app/services/config.service.ts b/src-ui/src/app/services/config.service.ts
index 8f8b84ac5..19158b3ce 100644
--- a/src-ui/src/app/services/config.service.ts
+++ b/src-ui/src/app/services/config.service.ts
@@ -20,6 +20,8 @@ export class ConfigService {
}
saveConfig(config: PaperlessConfig): Observable {
- return this.http.patch(this.baseUrl, config).pipe(first())
+ return this.http
+ .patch(`${this.baseUrl}${config.id}/`, config)
+ .pipe(first())
}
}