diff --git a/ui/src/app/components/form-object/form-object.component.html b/ui/src/app/components/form-object/form-object.component.html index 4c55d7d3e..61f08e1c0 100644 --- a/ui/src/app/components/form-object/form-object.component.html +++ b/ui/src/app/components/form-object/form-object.component.html @@ -5,7 +5,14 @@

{{ unionSpec.tag.name }}

{{ unionSpec.tag.name }} - + {{ unionSpec.tag['variant-names'][option] }} @@ -23,17 +30,21 @@ isEdited: entry.value.dirty }"> - - - - + + + + + - - - - - {{ spec.units }} + {{ spec.units }} @@ -43,7 +54,13 @@ {{ spec.name }} - + {{ spec['value-names'][option] }} @@ -146,7 +163,13 @@ - + + diff --git a/ui/src/app/components/form-object/form-object.component.ts b/ui/src/app/components/form-object/form-object.component.ts index 0d4e233dd..e37409595 100644 --- a/ui/src/app/components/form-object/form-object.component.ts +++ b/ui/src/app/components/form-object/form-object.component.ts @@ -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(`${text}`) } - handleInputChange (spec: ValueSpec) { - if (['string', 'number'].includes(spec.type)) { - this.onInputChange.emit() - } + handleInputChange () { + this.onInputChange.emit() } handleBooleanChange (key: string, spec: ValueSpecBoolean) { diff --git a/ui/src/app/modals/app-config/app-config.page.ts b/ui/src/app/modals/app-config/app-config.page.ts index 550143de9..a0e7f45ff 100644 --- a/ui/src/app/modals/app-config/app-config.page.ts +++ b/ui/src/app/modals/app-config/app-config.page.ts @@ -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', diff --git a/ui/src/app/modals/generic-form/generic-form.page.ts b/ui/src/app/modals/generic-form/generic-form.page.ts index 001e534a9..c73b9c649 100644 --- a/ui/src/app/modals/generic-form/generic-form.page.ts +++ b/ui/src/app/modals/generic-form/generic-form.page.ts @@ -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 { + convertToNumberRecursive(this.spec, this.formGroup) + if (this.formGroup.invalid) { this.formGroup.markAllAsTouched() document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' }) diff --git a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html index 346f7cb37..f96831c89 100644 --- a/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html +++ b/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces-item.component.html @@ -48,7 +48,7 @@

LAN Address

-

Service does not use a LAN Address

+

N/A

\ No newline at end of file diff --git a/ui/src/app/pkg-config/config-types.ts b/ui/src/app/pkg-config/config-types.ts index 3c0c0403d..9158ece8b 100644 --- a/ui/src/app/pkg-config/config-types.ts +++ b/ui/src/app/pkg-config/config-types.ts @@ -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 (t: ValueSpecList } export interface ListValueSpecString { - // @TODO add masked? pattern?: string 'pattern-description'?: string + masked: boolean + copyable: boolean } export interface ListValueSpecNumber { diff --git a/ui/src/app/services/api/api.fixures.ts b/ui/src/app/services/api/api.fixures.ts index 3138588a9..84194e2ed 100644 --- a/ui/src/app/services/api/api.fixures.ts +++ b/ui/src/app/services/api/api.fixures.ts @@ -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 diff --git a/ui/src/app/services/form.service.ts b/ui/src/app/services/form.service.ts index c2d0abf55..d50cc5e27 100644 --- a/ui/src/app/services/form.service.ts +++ b/ui/src/app/services/form.service.ts @@ -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) + }) + } + } + }) +}