- @for (option of getCategoryOptions(category); track option.key) {
-
-
-
-
-
- {{option.title}}
-
-
-
-
- @if (isSet(option.key)) {
-
+ @for (section of getCategorySections(category); track section) {
+ @if (section) {
+
{{section}}
+ }
+
+ @for (option of getCategoryOptions(category, section); track option.key) {
+
+
+
+
+
+ {{option.title}}
+
+
+
+
+ @if (isSet(option.key)) {
+
+ }
+
+
+ @switch (option.type) {
+ @case (ConfigOptionType.Select) {
}
+ @case (ConfigOptionType.Number) {
}
+ @case (ConfigOptionType.Boolean) {
}
+ @case (ConfigOptionType.String) {
}
+ @case (ConfigOptionType.JSON) {
}
+ @case (ConfigOptionType.File) {
}
+ @case (ConfigOptionType.Password) {
}
+ }
+
+ @if (option.note) {
+
{{option.note}}
}
-
- @switch (option.type) {
- @case (ConfigOptionType.Select) {
}
- @case (ConfigOptionType.Number) {
}
- @case (ConfigOptionType.Boolean) {
}
- @case (ConfigOptionType.String) {
}
- @case (ConfigOptionType.JSON) {
}
- @case (ConfigOptionType.File) {
}
- @case (ConfigOptionType.Password) {
}
- }
-
- @if (option.note) {
-
{{option.note}}
- }
-
- }
-
+ }
+
+ }
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 f4f4799a6..7925b6ad1 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
@@ -8,7 +8,11 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
-import { OutputTypeConfig } from 'src/app/data/paperless-config'
+import {
+ ConfigCategory,
+ ConfigSection,
+ OutputTypeConfig,
+} from 'src/app/data/paperless-config'
import { ConfigService } from 'src/app/services/config.service'
import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service'
@@ -158,4 +162,23 @@ describe('ConfigComponent', () => {
component.resetOption('barcodes_enabled')
expect(component.configForm.get('barcodes_enabled').value).toBeNull()
})
+
+ it('should group options into sections within a category, or not', () => {
+ const sections = component.getCategorySections(ConfigCategory.OCR)
+ expect(sections).toEqual([null, ConfigSection.RemoteOCR])
+ expect(
+ component
+ .getCategoryOptions(ConfigCategory.OCR)
+ .map((option) => option.key)
+ ).toContain('output_type')
+ expect(
+ component
+ .getCategoryOptions(ConfigCategory.OCR, ConfigSection.RemoteOCR)
+ .map((option) => option.key)
+ ).toEqual([
+ 'remote_ocr_engine',
+ 'remote_ocr_api_key',
+ 'remote_ocr_endpoint',
+ ])
+ })
})
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 9c2946438..0e3f432e2 100644
--- a/src-ui/src/app/components/admin/config/config.component.ts
+++ b/src-ui/src/app/components/admin/config/config.component.ts
@@ -74,8 +74,20 @@ export class ConfigComponent
return Object.values(ConfigCategory)
}
- getCategoryOptions(category: string): ConfigOption[] {
- return PaperlessConfigOptions.filter((o) => o.category === category)
+ getCategorySections(category: string): string[] {
+ return [
+ ...new Set(
+ PaperlessConfigOptions.filter((o) => o.category === category).map(
+ (o) => o.section ?? null // null means no section
+ )
+ ),
+ ]
+ }
+
+ getCategoryOptions(category: string, section: string = null): ConfigOption[] {
+ return PaperlessConfigOptions.filter(
+ (o) => o.category === category && (o.section ?? null) === section
+ )
}
initialConfig: PaperlessConfig
diff --git a/src-ui/src/app/data/paperless-config.ts b/src-ui/src/app/data/paperless-config.ts
index ad3f4c0c9..575525f1b 100644
--- a/src-ui/src/app/data/paperless-config.ts
+++ b/src-ui/src/app/data/paperless-config.ts
@@ -54,6 +54,10 @@ export const ConfigCategory = {
AI: $localize`AI Settings`,
}
+export const ConfigSection = {
+ RemoteOCR: $localize`Remote OCR`,
+}
+
export const LLMEmbeddingBackendConfig = {
OPENAI_LIKE: 'openai-like',
HUGGINGFACE: 'huggingface',
@@ -65,6 +69,10 @@ export const LLMBackendConfig = {
OLLAMA: 'ollama',
}
+export const RemoteOCREngineConfig = {
+ AZURE_AI: 'azureai',
+}
+
export interface ConfigOption {
key: string
title: string
@@ -72,6 +80,7 @@ export interface ConfigOption {
choices?: Array<{ id: string; name: string }>
config_key?: string
category: string
+ section?: string
note?: string
}
@@ -181,6 +190,33 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_OCR_USER_ARGS',
category: ConfigCategory.OCR,
},
+ {
+ key: 'remote_ocr_engine',
+ title: $localize`Remote OCR Engine`,
+ type: ConfigOptionType.Select,
+ choices: mapToItems(RemoteOCREngineConfig),
+ config_key: 'PAPERLESS_REMOTE_OCR_ENGINE',
+ category: ConfigCategory.OCR,
+ section: ConfigSection.RemoteOCR,
+ note: $localize`Enabling remote OCR sends documents to a third-party service for processing. Consider the privacy implications as well as potential costs before enabling.`,
+ },
+ {
+ key: 'remote_ocr_api_key',
+ title: $localize`Remote OCR API Key`,
+ type: ConfigOptionType.Password,
+ config_key: 'PAPERLESS_REMOTE_OCR_API_KEY',
+ category: ConfigCategory.OCR,
+ section: ConfigSection.RemoteOCR,
+ },
+ {
+ key: 'remote_ocr_endpoint',
+ title: $localize`Remote OCR Endpoint`,
+ type: ConfigOptionType.String,
+ config_key: 'PAPERLESS_REMOTE_OCR_ENDPOINT',
+ category: ConfigCategory.OCR,
+ section: ConfigSection.RemoteOCR,
+ note: $localize`Required when using the Azure AI engine.`,
+ },
{
key: 'app_logo',
title: $localize`Application Logo`,
@@ -398,6 +434,9 @@ export interface PaperlessConfig extends ObjectWithId {
barcode_enable_tag: boolean
barcode_tag_mapping: object
barcode_tag_split: boolean
+ remote_ocr_engine: string
+ remote_ocr_api_key: string
+ remote_ocr_endpoint: string
ai_enabled: boolean
llm_embedding_backend: string
llm_embedding_model: string