convert strigs to numbers for numberic form controls

This commit is contained in:
Matt Hill
2021-09-29 13:25:24 -06:00
committed by Aiden McClelland
parent ef1cd70fbc
commit 2dc0f97d90
8 changed files with 143 additions and 78 deletions

View File

@@ -5,7 +5,14 @@
<p class="input-label">{{ unionSpec.tag.name }}</p>
<ion-item>
<ion-label>{{ unionSpec.tag.name }}</ion-label>
<ion-select [interfaceOptions]="{ message: getWarningText(unionSpec.warning) }" slot="end" placeholder="Select" [formControlName]="unionSpec.tag.id" [selectedText]="unionSpec.tag['variant-names'][entry.value.value]" (ionChange)="updateUnion($event)">
<ion-select
[interfaceOptions]="{ message: getWarningText(unionSpec.warning) }"
slot="end"
placeholder="Select"
[formControlName]="unionSpec.tag.id"
[selectedText]="unionSpec.tag['variant-names'][entry.value.value]"
(ionChange)="updateUnion($event)"
>
<ion-select-option *ngFor="let option of Object.keys(unionSpec.variants)" [value]="option">
{{ unionSpec.tag['variant-names'][option] }}
</ion-select-option>
@@ -23,17 +30,21 @@
isEdited: entry.value.dirty
}"></form-label>
</h4>
<!-- string -->
<ion-item color="dark" *ngIf="spec.type === 'string'">
<ion-input [type]="spec.masked && !unmasked[entry.key] ? 'password' : 'text'" [placeholder]="'Enter ' + spec.name" [formControlName]="entry.key" (ionFocus)="presentAlertChangeWarning(entry.key, spec)" (ionChange)="handleInputChange(spec)"></ion-input>
<ion-button *ngIf="spec.masked" fill="clear" color="light" (click)="unmasked[entry.key] = !unmasked[entry.key]">
<!-- string or number -->
<ion-item color="dark" *ngIf="spec.type === 'string' || spec.type === 'number'">
<ion-input
[type]="spec.type === 'string' && spec.masked && !unmasked[entry.key] ? 'password' : 'text'"
[inputmode]="spec.type === 'number' ? 'tel' : 'text'"
[placeholder]="'Enter ' + spec.name"
[formControlName]="entry.key"
(ionFocus)="presentAlertChangeWarning(entry.key, spec)"
(ionChange)="handleInputChange()"
>
</ion-input>
<ion-button *ngIf="spec.type === 'string' && spec.masked" slot="end" fill="clear" color="light" (click)="unmasked[entry.key] = !unmasked[entry.key]">
<ion-icon slot="icon-only" [name]="unmasked[entry.key] ? 'eye-off-outline' : 'eye-outline'" size="small"></ion-icon>
</ion-button>
</ion-item>
<!-- number -->
<ion-item color="dark" *ngIf="spec.type === 'number'">
<ion-input type="tel" [placeholder]="'Enter ' + spec.name" [formControlName]="entry.key" (ionFocus)="presentAlertChangeWarning(entry.key, spec)" (ionChange)="handleInputChange(spec)"></ion-input>
<ion-note *ngIf="spec.units" slot="end" color="light" style="font-size: medium;">{{ spec.units }}</ion-note>
<ion-note *ngIf="spec.type === 'number' && spec.units" slot="end" color="light" style="font-size: medium;">{{ spec.units }}</ion-note>
</ion-item>
<!-- boolean -->
<ion-item *ngIf="spec.type === 'boolean'">
@@ -43,7 +54,13 @@
<!-- enum -->
<ion-item *ngIf="spec.type === 'enum'">
<ion-label>{{ spec.name }}</ion-label>
<ion-select [interfaceOptions]="{ message: getWarningText(spec.warning) }" slot="end" placeholder="Select" [formControlName]="entry.key" [selectedText]="spec['value-names'][formGroup.get(entry.key).value]" (ionChange)="handleInputChange(spec)">
<ion-select
[interfaceOptions]="{ message: getWarningText(spec.warning) }"
slot="end"
placeholder="Select"
[formControlName]="entry.key"
[selectedText]="spec['value-names'][formGroup.get(entry.key).value]"
>
<ion-select-option *ngFor="let option of spec.values" [value]="option">
{{ spec['value-names'][option] }}
</ion-select-option>
@@ -146,7 +163,13 @@
<!-- string or number -->
<ion-item-group *ngIf="spec.subtype === 'string' || spec.subtype === 'number'">
<ion-item color="dark">
<ion-input type="spec.spec.masked ? 'password' : 'text'" [placeholder]="'Enter ' + spec.name" [formControlName]="i"></ion-input>
<ion-input
[type]="$any(spec.spec).masked ? 'password' : 'text'"
[inputmode]="spec.subtype === 'number' ? 'tel' : 'text'"
[placeholder]="'Enter ' + spec.name"
[formControlName]="i"
>
</ion-input>
<ion-button slot="end" color="danger" (click)="presentAlertDelete(entry.key, i)">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>

View File

@@ -1,7 +1,7 @@
import { Component, Input, Output, SimpleChange, EventEmitter } from '@angular/core'
import { AbstractFormGroupDirective, FormArray, FormGroup } from '@angular/forms'
import { AbstractControl, AbstractFormGroupDirective, FormArray, FormGroup } from '@angular/forms'
import { AlertButton, AlertController, IonicSafeString, ModalController } from '@ionic/angular'
import { ConfigSpec, ListValueSpecOf, ValueSpec, ValueSpecBoolean, ValueSpecList, ValueSpecListOf, ValueSpecUnion } from 'src/app/pkg-config/config-types'
import { ConfigSpec, ListValueSpecOf, ListValueSpecString, ValueSpec, ValueSpecBoolean, ValueSpecEnum, ValueSpecList, ValueSpecListOf, ValueSpecNumber, ValueSpecString, ValueSpecUnion } from 'src/app/pkg-config/config-types'
import { FormService } from 'src/app/services/form.service'
import { Range } from 'src/app/pkg-config/config-utilities'
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
@@ -109,10 +109,8 @@ export class FormObjectComponent {
if (text) return new IonicSafeString(`<ion-text color="warning">${text}</ion-text>`)
}
handleInputChange (spec: ValueSpec) {
if (['string', 'number'].includes(spec.type)) {
this.onInputChange.emit()
}
handleInputChange () {
this.onInputChange.emit()
}
handleBooleanChange (key: string, spec: ValueSpecBoolean) {

View File

@@ -4,12 +4,12 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { isEmptyObject, isObject, Recommendation } from 'src/app/util/misc.util'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import { ConfigSpec, ListValueSpecObject, ListValueSpecUnion } from 'src/app/pkg-config/config-types'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { ErrorToastService } from 'src/app/services/error-toast.service'
import { FormGroup } from '@angular/forms'
import { FormService } from 'src/app/services/form.service'
import { FormArray, FormGroup } from '@angular/forms'
import { convertToNumberRecursive, FormService } from 'src/app/services/form.service'
@Component({
selector: 'app-config',
@@ -71,37 +71,6 @@ export class AppConfigPage {
this.content.scrollToPoint(undefined, 1)
}
setConfig (spec: ConfigSpec, config: object, depConfig?: object) {
this.configSpec = spec
this.current = config
this.configForm = this.formService.createForm(spec, { ...config, ...depConfig })
this.configForm.markAllAsTouched()
if (depConfig) {
this.alterConfigRecursive(this.configForm, depConfig)
}
}
alterConfigRecursive (group: FormGroup, config: object) {
Object.keys(config).forEach(key => {
const next = group.get(key)
if (!next) throw new Error('Dependency config not compatible with service version. Please contact support')
const newVal = config[key]
// check if val is an object
if (isObject(newVal)) {
this.alterConfigRecursive(next as FormGroup, newVal)
} else {
let val1 = group.get(key).value
let val2 = config[key]
if (Array.isArray(newVal)) {
val1 = JSON.stringify(val1)
val2 = JSON.stringify(val2)
}
if (val1 != val2) next.markAsDirty()
}
})
}
resetDefaults () {
this.configForm = this.formService.createForm(this.configSpec)
this.alterConfigRecursive(this.configForm, this.current)
@@ -120,6 +89,10 @@ export class AppConfigPage {
}
async save () {
convertToNumberRecursive(this.configSpec, this.configForm)
console.log('SAVING', this.configForm.value)
if (this.configForm.invalid) {
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })
return
@@ -132,10 +105,11 @@ export class AppConfigPage {
})
await loader.present()
const config = this.configForm.value
this.saving = true
try {
this.saving = true
const config = this.configForm.value
const breakages = await this.embassyApi.drySetPackageConfig({
id: this.pkg.manifest.id,
config,
@@ -165,6 +139,37 @@ export class AppConfigPage {
}
}
private setConfig (spec: ConfigSpec, config: object, depConfig?: object) {
this.configSpec = spec
this.current = config
this.configForm = this.formService.createForm(spec, { ...config, ...depConfig })
this.configForm.markAllAsTouched()
if (depConfig) {
this.alterConfigRecursive(this.configForm, depConfig)
}
}
private alterConfigRecursive (group: FormGroup, config: object) {
Object.keys(config).forEach(key => {
const next = group.get(key)
if (!next) throw new Error('Dependency config not compatible with service version. Please contact support')
const newVal = config[key]
// check if val is an object
if (isObject(newVal)) {
this.alterConfigRecursive(next as FormGroup, newVal)
} else {
let val1 = group.get(key).value
let val2 = config[key]
if (Array.isArray(newVal)) {
val1 = JSON.stringify(val1)
val2 = JSON.stringify(val2)
}
if (val1 != val2) next.markAsDirty()
}
})
}
private async presentAlertUnsaved () {
const alert = await this.alertCtrl.create({
header: 'Unsaved Changes',

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'
import { FormGroup } from '@angular/forms'
import { ModalController } from '@ionic/angular'
import { FormService } from 'src/app/services/form.service'
import { convertToNumberRecursive, FormService } from 'src/app/services/form.service'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
export interface ActionButton {
@@ -40,6 +40,8 @@ export class GenericFormPage {
}
async handleClick (handler: ActionButton['handler']): Promise<void> {
convertToNumberRecursive(this.spec, this.formGroup)
if (this.formGroup.invalid) {
this.formGroup.markAllAsTouched()
document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' })

View File

@@ -48,7 +48,7 @@
<ion-item *ngIf="!interface.addresses['lan-address']">
<ion-label>
<h2>LAN Address</h2>
<p>Service does not use a LAN Address</p>
<p>N/A</p>
</ion-label>
</ion-item>
</div>

View File

@@ -19,8 +19,6 @@ export interface ValueSpecString extends ListValueSpecString, WithStandalone {
type: 'string'
default?: DefaultString
nullable: boolean
masked: boolean
copyable: boolean
}
export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone {
@@ -88,9 +86,10 @@ export function isValueSpecListOf<S extends ListValueSpecType> (t: ValueSpecList
}
export interface ListValueSpecString {
// @TODO add masked?
pattern?: string
'pattern-description'?: string
masked: boolean
copyable: boolean
}
export interface ListValueSpecNumber {

View File

@@ -1135,7 +1135,7 @@ export module Mock {
'range': '(-100,100]',
'units': 'BTC',
},
'secondaryNumbers': {
'unluckyNumbers': {
'name': 'Unlucky Numbers',
'type': 'list',
'subtype': 'number',
@@ -1345,6 +1345,8 @@ export module Mock {
'192.168.1.1',
],
'spec': {
'masked': false,
'copyable': false,
'pattern': '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))',
'pattern-description': 'must be a valid ipv4, ipv6, or domain name',
},
@@ -1356,7 +1358,10 @@ export module Mock {
'description': 'api keys that are authorized to access your Bitcoin node.',
'range': '[0,*)',
'default': [],
'spec': { },
'spec': {
'masked': false,
'copyable': false,
},
},
},
// actual config

View File

@@ -17,14 +17,6 @@ export class FormService {
return this.getFormGroup(config, [], current)
}
getListItemValidators (spec: ValueSpecList) {
if (isValueSpecListOf(spec, 'string')) {
return this.stringValidators(spec.spec)
} else if (isValueSpecListOf(spec, 'number')) {
return this.numberValidators(spec.spec)
}
}
getUnionObject (spec: ValueSpecUnion | ListValueSpecUnion, selection: string, current?: { [key: string]: any }): FormGroup {
const { variants, tag } = spec
const { name, description, warning } = isFullUnion(spec) ? spec : { ...spec.tag, warning: undefined }
@@ -41,15 +33,6 @@ export class FormService {
return this.getFormGroup({ [spec.tag.id]: enumSpec, ...spec.variants[selection] }, [], current)
}
getFormGroup (config: ConfigSpec, validators: ValidatorFn[] = [], current: { [key: string]: any } = { }): FormGroup {
let group = { }
Object.entries(config).map(([key, spec]) => {
if (spec.type === 'pointer') return
group[key] = this.getFormEntry(key, spec, current ? current[key] : { })
})
return this.formBuilder.group(group, { validators } )
}
getListItem (spec: ValueSpecList, entry: any) {
const listItemValidators = this.getListItemValidators(spec)
if (isValueSpecListOf(spec, 'string')) {
@@ -65,6 +48,23 @@ export class FormService {
}
}
private getListItemValidators (spec: ValueSpecList) {
if (isValueSpecListOf(spec, 'string')) {
return this.stringValidators(spec.spec)
} else if (isValueSpecListOf(spec, 'number')) {
return this.numberValidators(spec.spec)
}
}
private getFormGroup (config: ConfigSpec, validators: ValidatorFn[] = [], current: { [key: string]: any } = { }): FormGroup {
let group = { }
Object.entries(config).map(([key, spec]) => {
if (spec.type === 'pointer') return
group[key] = this.getFormEntry(key, spec, current ? current[key] : { })
})
return this.formBuilder.group(group, { validators } )
}
private getFormEntry (key: string, spec: ValueSpec, currentValue: any): FormGroup | FormArray | FormControl {
let validators: ValidatorFn[]
let value: any
@@ -405,3 +405,36 @@ const sampleUniqueBy: UniqueBy = {
] },
],
}
export function convertToNumberRecursive (configSpec: ConfigSpec, group: FormGroup) {
Object.entries(configSpec).forEach(([key, valueSpec]) => {
if (valueSpec.type === 'number') {
const control = group.get(key)
control.setValue(Number(control.value))
} else if (valueSpec.type === 'object') {
convertToNumberRecursive(valueSpec.spec, group.get(key) as FormGroup)
} else if (valueSpec.type === 'union') {
const control = group.get(key) as FormGroup
const spec = valueSpec.variants[control.controls[valueSpec.tag.id].value]
convertToNumberRecursive(spec, control)
} else if (valueSpec.type === 'list') {
const formArr = group.get(key) as FormArray
if (valueSpec.subtype === 'number') {
formArr.controls.forEach(control => {
control.setValue(Number(control.value))
})
} else if (valueSpec.subtype === 'object') {
formArr.controls.forEach((formGroup: FormGroup) => {
const objectSpec = valueSpec.spec as ListValueSpecObject
convertToNumberRecursive(objectSpec.spec, formGroup)
})
} else if (valueSpec.subtype === 'union') {
formArr.controls.forEach((formGroup: FormGroup) => {
const unionSpec = valueSpec.spec as ListValueSpecUnion
const spec = unionSpec.variants[formGroup.controls[unionSpec.tag.id].value]
convertToNumberRecursive(spec, formGroup)
})
}
}
})
}