Use password and select config fields

This commit is contained in:
shamoon 2025-04-24 13:54:42 -07:00
parent e14f508327
commit a3eed49638
No known key found for this signature in database
6 changed files with 62 additions and 43 deletions

View File

@ -35,6 +35,7 @@
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> } @case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> } @case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> } @case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
@case (ConfigOptionType.Password) { <pngx-input-password [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-password> }
} }
</div> </div>
</div> </div>

View File

@ -29,6 +29,7 @@ import { SettingsService } from 'src/app/services/settings.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { FileComponent } from '../../common/input/file/file.component' import { FileComponent } from '../../common/input/file/file.component'
import { NumberComponent } from '../../common/input/number/number.component' import { NumberComponent } from '../../common/input/number/number.component'
import { PasswordComponent } from '../../common/input/password/password.component'
import { SelectComponent } from '../../common/input/select/select.component' import { SelectComponent } from '../../common/input/select/select.component'
import { SwitchComponent } from '../../common/input/switch/switch.component' import { SwitchComponent } from '../../common/input/switch/switch.component'
import { TextComponent } from '../../common/input/text/text.component' import { TextComponent } from '../../common/input/text/text.component'
@ -46,6 +47,7 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
TextComponent, TextComponent,
NumberComponent, NumberComponent,
FileComponent, FileComponent,
PasswordComponent,
AsyncPipe, AsyncPipe,
NgbNavModule, NgbNavModule,
FormsModule, FormsModule,

View File

@ -1,17 +1,24 @@
<div class="mb-3"> <div class="mb-3" [class.pb-3]="error">
<label class="form-label" [for]="inputId">{{title}}</label> <div class="row">
<div class="input-group" [class.is-invalid]="error"> <div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete"> @if (title) {
@if (showReveal) { <label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle"> }
<i-bs name="eye"></i-bs> </div>
</button> <div class="position-relative" [class.col-md-9]="horizontal">
<div class="input-group" [class.is-invalid]="error">
<input #inputField [type]="showReveal && textVisible ? 'text' : 'password'" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (focus)="onFocus()" (focusout)="onFocusOut()" (change)="onChange(value)" [disabled]="disabled" [autocomplete]="autocomplete">
@if (showReveal) {
<button type="button" class="btn btn-outline-secondary" (click)="toggleVisibility()" i18n-title title="Show password" [disabled]="disabled || disableRevealToggle">
<i-bs name="eye"></i-bs>
</button>
}
</div>
<div class="invalid-feedback">
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
} }
</div> </div>
<div class="invalid-feedback">
{{error}}
</div>
@if (hint) {
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
}
</div> </div>

View File

@ -44,6 +44,7 @@ export enum ConfigOptionType {
Boolean = 'boolean', Boolean = 'boolean',
JSON = 'json', JSON = 'json',
File = 'file', File = 'file',
Password = 'password',
} }
export const ConfigCategory = { export const ConfigCategory = {
@ -52,6 +53,11 @@ export const ConfigCategory = {
AI: $localize`AI Settings`, AI: $localize`AI Settings`,
} }
export const LLMBackendConfig = {
OPENAI: 'openai',
OLLAMA: 'ollama',
}
export interface ConfigOption { export interface ConfigOption {
key: string key: string
title: string title: string
@ -191,7 +197,8 @@ export const PaperlessConfigOptions: ConfigOption[] = [
{ {
key: 'llm_backend', key: 'llm_backend',
title: $localize`LLM Backend`, title: $localize`LLM Backend`,
type: ConfigOptionType.String, type: ConfigOptionType.Select,
choices: mapToItems(LLMBackendConfig),
config_key: 'PAPERLESS_LLM_BACKEND', config_key: 'PAPERLESS_LLM_BACKEND',
category: ConfigCategory.AI, category: ConfigCategory.AI,
}, },
@ -205,7 +212,7 @@ export const PaperlessConfigOptions: ConfigOption[] = [
{ {
key: 'llm_api_key', key: 'llm_api_key',
title: $localize`LLM API Key`, title: $localize`LLM API Key`,
type: ConfigOptionType.String, type: ConfigOptionType.Password,
config_key: 'PAPERLESS_LLM_API_KEY', config_key: 'PAPERLESS_LLM_API_KEY',
category: ConfigCategory.AI, category: ConfigCategory.AI,
}, },

View File

@ -32,33 +32,31 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.maxDiff = None self.maxDiff = None
self.assertEqual( self.assertDictEqual(
json.dumps(response.data[0]), response.data[0],
json.dumps( {
{ "id": 1,
"id": 1, "user_args": None,
"user_args": None, "output_type": None,
"output_type": None, "pages": None,
"pages": None, "language": None,
"language": None, "mode": None,
"mode": None, "skip_archive_file": None,
"skip_archive_file": None, "image_dpi": None,
"image_dpi": None, "unpaper_clean": None,
"unpaper_clean": None, "deskew": None,
"deskew": None, "rotate_pages": None,
"rotate_pages": None, "rotate_pages_threshold": None,
"rotate_pages_threshold": None, "max_image_pixels": None,
"max_image_pixels": None, "color_conversion_strategy": None,
"color_conversion_strategy": None, "app_title": None,
"app_title": None, "app_logo": None,
"app_logo": None, "ai_enabled": False,
"ai_enabled": False, "llm_backend": None,
"llm_backend": None, "llm_model": None,
"llm_model": None, "llm_api_key": None,
"llm_api_key": None, "llm_url": None,
"llm_url": None, },
},
),
) )
def test_api_get_ui_settings_with_config(self): def test_api_get_ui_settings_with_config(self):

View File

@ -185,6 +185,10 @@ class ProfileSerializer(serializers.ModelSerializer):
class ApplicationConfigurationSerializer(serializers.ModelSerializer): class ApplicationConfigurationSerializer(serializers.ModelSerializer):
user_args = serializers.JSONField(binary=True, allow_null=True) user_args = serializers.JSONField(binary=True, allow_null=True)
llm_api_key = ObfuscatedPasswordField(
required=False,
allow_null=True,
)
def run_validation(self, data): def run_validation(self, data):
# Empty strings treated as None to avoid unexpected behavior # Empty strings treated as None to avoid unexpected behavior