@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.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>
import { ToastService } from 'src/app/services/toast.service'
import { FileComponent } from '../../common/input/file/file.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 { SwitchComponent } from '../../common/input/switch/switch.component'
import { TextComponent } from '../../common/input/text/text.component'
TextComponent,
NumberComponent,
FileComponent,
+ PasswordComponent,
AsyncPipe,
NgbNavModule,
FormsModule,
-<div class="mb-3">
- <label class="form-label" [for]="inputId">{{title}}</label>
- <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 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>
+ }
+ </div>
+ <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 class="invalid-feedback">
- {{error}}
- </div>
- @if (hint) {
- <small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
- }
</div>
Boolean = 'boolean',
JSON = 'json',
File = 'file',
+ Password = 'password',
}
export const ConfigCategory = {
AI: $localize`AI Settings`,
}
+export const LLMBackendConfig = {
+ OPENAI: 'openai',
+ OLLAMA: 'ollama',
+}
+
export interface ConfigOption {
key: string
title: string
{
key: 'llm_backend',
title: $localize`LLM Backend`,
- type: ConfigOptionType.String,
+ type: ConfigOptionType.Select,
+ choices: mapToItems(LLMBackendConfig),
config_key: 'PAPERLESS_LLM_BACKEND',
category: ConfigCategory.AI,
},
{
key: 'llm_api_key',
title: $localize`LLM API Key`,
- type: ConfigOptionType.String,
+ type: ConfigOptionType.Password,
config_key: 'PAPERLESS_LLM_API_KEY',
category: ConfigCategory.AI,
},
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.maxDiff = None
- self.assertEqual(
- json.dumps(response.data[0]),
- json.dumps(
- {
- "id": 1,
- "user_args": None,
- "output_type": None,
- "pages": None,
- "language": None,
- "mode": None,
- "skip_archive_file": None,
- "image_dpi": None,
- "unpaper_clean": None,
- "deskew": None,
- "rotate_pages": None,
- "rotate_pages_threshold": None,
- "max_image_pixels": None,
- "color_conversion_strategy": None,
- "app_title": None,
- "app_logo": None,
- "ai_enabled": False,
- "llm_backend": None,
- "llm_model": None,
- "llm_api_key": None,
- "llm_url": None,
- },
- ),
+ self.assertDictEqual(
+ response.data[0],
+ {
+ "id": 1,
+ "user_args": None,
+ "output_type": None,
+ "pages": None,
+ "language": None,
+ "mode": None,
+ "skip_archive_file": None,
+ "image_dpi": None,
+ "unpaper_clean": None,
+ "deskew": None,
+ "rotate_pages": None,
+ "rotate_pages_threshold": None,
+ "max_image_pixels": None,
+ "color_conversion_strategy": None,
+ "app_title": None,
+ "app_logo": None,
+ "ai_enabled": False,
+ "llm_backend": None,
+ "llm_model": None,
+ "llm_api_key": None,
+ "llm_url": None,
+ },
)
def test_api_get_ui_settings_with_config(self):
class ApplicationConfigurationSerializer(serializers.ModelSerializer):
user_args = serializers.JSONField(binary=True, allow_null=True)
+ llm_api_key = ObfuscatedPasswordField(
+ required=False,
+ allow_null=True,
+ )
def run_validation(self, data):
# Empty strings treated as None to avoid unexpected behavior