diff --git a/ui/src/app/components/install-wizard/dependents/dependents.component.html b/ui/src/app/components/install-wizard/dependents/dependents.component.html index d0b4ee574..0671f3c99 100644 --- a/ui/src/app/components/install-wizard/dependents/dependents.component.html +++ b/ui/src/app/components/install-wizard/dependents/dependents.component.html @@ -20,10 +20,8 @@ {{ longMessage }}
-
- Will Stop +
+ Affected Services
{ this.hasDependentViolation = this.dependentBreakages && !isEmptyObject(this.dependentBreakages) if (this.hasDependentViolation) { - this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will cause the following services to STOP running. Starting them again will require additional actions.` + this.longMessage = `${capitalizeFirstLetter(this.params.verb)} ${this.params.title} will prohibit the following services from functioning properly and will cause them to stop if they are currently running.` this.color$.next('warning') } else if (this.params.skipConfirmationDialogue) { this.transitions.next() diff --git a/ui/src/app/components/install-wizard/prebaked-wizards.ts b/ui/src/app/components/install-wizard/prebaked-wizards.ts index d961c2400..c928e00f4 100644 --- a/ui/src/app/components/install-wizard/prebaked-wizards.ts +++ b/ui/src/app/components/install-wizard/prebaked-wizards.ts @@ -202,7 +202,7 @@ export class WizardBaker { action, verb: 'uninstalling', title, - fetchBreakages: () => this.embassyApi.dryRemovePackage({ id }).then(breakages => breakages), + fetchBreakages: () => this.embassyApi.dryUninstallPackage({ id }).then(breakages => breakages), }, }, bottomBar: { cancel: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, next: 'Uninstall' }, @@ -214,7 +214,7 @@ export class WizardBaker { action, verb: 'uninstalling', title, - executeAction: () => this.embassyApi.removePackage({ id }), + executeAction: () => this.embassyApi.uninstallPackage({ id }), }, }, bottomBar: { finish: 'Dismiss', cancel: { whileLoading: { } } }, 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 9718b6877..72998e9f2 100644 --- a/ui/src/app/modals/app-config/app-config.page.ts +++ b/ui/src/app/modals/app-config/app-config.page.ts @@ -9,7 +9,7 @@ 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 { convertToNumberRecursive, FormService } from 'src/app/services/form.service' +import { convertValuesRecursive, FormService } from 'src/app/services/form.service' @Component({ selector: 'app-config', @@ -87,9 +87,7 @@ export class AppConfigPage { } async save () { - convertToNumberRecursive(this.configSpec, this.configForm) - - console.log('SAVING', this.configForm.value) + convertValuesRecursive(this.configSpec, this.configForm) if (this.configForm.invalid) { document.getElementsByClassName('validation-error')[0].parentElement.parentElement.scrollIntoView({ behavior: 'smooth' }) 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 c73b9c649..f106fd608 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 { convertToNumberRecursive, FormService } from 'src/app/services/form.service' +import { convertValuesRecursive, FormService } from 'src/app/services/form.service' import { ConfigSpec } from 'src/app/pkg-config/config-types' export interface ActionButton { @@ -40,7 +40,7 @@ export class GenericFormPage { } async handleClick (handler: ActionButton['handler']): Promise { - convertToNumberRecursive(this.spec, this.formGroup) + convertValuesRecursive(this.spec, this.formGroup) if (this.formGroup.invalid) { this.formGroup.markAllAsTouched() diff --git a/ui/src/app/modals/generic-input/generic-input.component.html b/ui/src/app/modals/generic-input/generic-input.component.html index 50f1f37ad..5df9aa9af 100644 --- a/ui/src/app/modals/generic-input/generic-input.component.html +++ b/ui/src/app/modals/generic-input/generic-input.component.html @@ -10,7 +10,7 @@
-
+

{{ label }}

@@ -19,9 +19,7 @@ -

- {{ error || 'placeholder' }} -

+

{{ error }}

diff --git a/ui/src/app/modals/generic-input/generic-input.component.ts b/ui/src/app/modals/generic-input/generic-input.component.ts index b76cc52c7..6adae6fb1 100644 --- a/ui/src/app/modals/generic-input/generic-input.component.ts +++ b/ui/src/app/modals/generic-input/generic-input.component.ts @@ -1,6 +1,5 @@ import { Component, Input } from '@angular/core' -import { IonicSafeString, LoadingController, ModalController } from '@ionic/angular' -import { getErrorMessage } from 'src/app/services/error-toast.service' +import { ModalController } from '@ionic/angular' @Component({ selector: 'generic-input', @@ -18,11 +17,10 @@ export class GenericInputComponent { @Input() value = '' @Input() submitFn: (value: string) => Promise unmasked = false - error: string | IonicSafeString + error: string constructor ( private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, ) { } toggleMask () { @@ -34,19 +32,8 @@ export class GenericInputComponent { } async submit () { - const loader = await this.loadingCtrl.create({ - spinner: 'lines', - cssClass: 'loader', - }) - loader.present() - - try { - await this.submitFn(this.value) - this.modalCtrl.dismiss(undefined, 'success') - } catch (e) { - this.error = getErrorMessage(e) - } finally { - loader.dismiss() - } + // @TODO validate input? + await this.submitFn(this.value) + this.modalCtrl.dismiss(undefined, 'success') } } diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.html b/ui/src/app/pages/apps-routes/app-list/app-list.page.html index 9ae5966b6..9e58787c3 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.html +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.html @@ -14,7 +14,7 @@
-
+

Welcome to Embassy

Get started by installing your first service.

diff --git a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts index daea2d6fe..3ed092d6c 100644 --- a/ui/src/app/pages/apps-routes/app-list/app-list.page.ts +++ b/ui/src/app/pages/apps-routes/app-list/app-list.page.ts @@ -21,11 +21,12 @@ export class AppListPage { connectionFailure: boolean pkgs: { [id: string]: PkgInfo } = { } loading = true + empty = false constructor ( private readonly config: ConfigService, private readonly connectionService: ConnectionService, - private readonly installPackageService: PackageLoadingService, + private readonly pkgLoading: PackageLoadingService, public readonly patch: PatchDbService, ) { } @@ -34,11 +35,7 @@ export class AppListPage { this.patch.watch$('package-data') .pipe( filter(obj => { - return obj && - ( - isEmptyObject(obj) || - Object.keys(obj).length !== Object.keys(this.pkgs).length - ) + return obj && Object.keys(obj).length !== Object.keys(this.pkgs).length }), ) .subscribe(pkgs => { @@ -46,6 +43,8 @@ export class AppListPage { const ids = Object.keys(pkgs) + this.empty = !ids.length + Object.keys(this.pkgs).forEach(id => { if (!ids.includes(id)) { this.pkgs[id].sub.unsubscribe() @@ -59,7 +58,7 @@ export class AppListPage { this.pkgs[id] = { entry: pkgs[id], primaryRendering: PrimaryRendering[renderPkgStatus(pkgs[id]).primary], - installProgress: !isEmptyObject(pkgs[id]['install-progress']) ? this.installPackageService.transform(pkgs[id]['install-progress']) : undefined, + installProgress: !isEmptyObject(pkgs[id]['install-progress']) ? this.pkgLoading.transform(pkgs[id]['install-progress']) : undefined, error: false, sub: null, } @@ -69,7 +68,7 @@ export class AppListPage { const statuses = renderPkgStatus(pkg) const primaryRendering = PrimaryRendering[statuses.primary] this.pkgs[id].entry = pkg - this.pkgs[id].installProgress = !isEmptyObject(pkg['install-progress']) ? this.installPackageService.transform(pkg['install-progress']) : undefined + this.pkgs[id].installProgress = !isEmptyObject(pkg['install-progress']) ? this.pkgLoading.transform(pkg['install-progress']) : undefined this.pkgs[id].primaryRendering = primaryRendering this.pkgs[id].error = statuses.health === HealthStatus.Failure || [DependencyStatus.Issue, DependencyStatus.Critical].includes(statuses.dependency) }) diff --git a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index 4d68447b6..dbde1f305 100644 --- a/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -13,8 +13,7 @@ import { DependencyStatus, HealthStatus, PrimaryRendering, PrimaryStatus, render import { ConnectionFailure, ConnectionService } from 'src/app/services/connection.service' import { ErrorToastService } from 'src/app/services/error-toast.service' import { AppConfigPage } from 'src/app/modals/app-config/app-config.page' -import { PackageLoadingService } from 'src/app/services/package-loading.service' -import { ProgressData } from 'src/app/pipes/install-state.pipe' +import { PackageLoadingService, ProgressData } from 'src/app/services/package-loading.service' @Component({ selector: 'app-show', diff --git a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts index 30b435a20..cfbce9de0 100644 --- a/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts +++ b/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -47,7 +47,10 @@ export class ServerMetricsPage { async getMetrics (): Promise { try { - this.metrics = await this.embassyApi.getServerMetrics({ }) + const metrics = await this.embassyApi.getServerMetrics({ }) + Object.entries(metrics).forEach(([key, val]) => { + this.metrics[key] = val + }) } catch (e) { this.errToast.present(e) this.stopDaemon() diff --git a/ui/src/app/pipes/install-state.pipe.ts b/ui/src/app/pipes/install-state.pipe.ts index bddc54231..6f000e120 100644 --- a/ui/src/app/pipes/install-state.pipe.ts +++ b/ui/src/app/pipes/install-state.pipe.ts @@ -1,4 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core' +import { PackageLoadingService, ProgressData } from '../services/package-loading.service' import { InstallProgress } from '../services/patch-db/data-model' @Pipe({ @@ -6,37 +7,11 @@ import { InstallProgress } from '../services/patch-db/data-model' }) export class InstallState implements PipeTransform { + constructor ( + private readonly pkgLoading: PackageLoadingService, + ) { } + transform (loadData: InstallProgress): ProgressData { - let { downloaded, validated, unpacked, size, 'download-complete': downloadComplete, 'validation-complete': validationComplete, 'unpack-complete': unpackComplete } = loadData - downloaded = downloadComplete ? size : downloaded - validated = validationComplete ? size : validated - unpacked = unpackComplete ? size : unpacked - - const downloadWeight = 1 - const validateWeight = .2 - const unpackWeight = .7 - - const numerator = Math.floor( - downloadWeight * downloaded + - validateWeight * validated + - unpackWeight * unpacked) - - const denominator = Math.floor(loadData.size * (downloadWeight + validateWeight + unpackWeight)) - - return { - totalProgress: Math.round(100 * numerator / denominator), - downloadProgress: Math.round(100 * downloaded / size), - validateProgress: Math.round(100 * validated / size), - unpackProgress: Math.round(100 * unpacked / size), - isComplete: downloadComplete && validationComplete && unpackComplete, - } + return this.pkgLoading.transform(loadData) } } - -export interface ProgressData { - totalProgress: number - downloadProgress: number - validateProgress: number - unpackProgress: number - isComplete: boolean -} \ No newline at end of file diff --git a/ui/src/app/pkg-config/config-utilities.ts b/ui/src/app/pkg-config/config-utilities.ts index e8ce7b14a..a7a0ba49a 100644 --- a/ui/src/app/pkg-config/config-utilities.ts +++ b/ui/src/app/pkg-config/config-utilities.ts @@ -6,7 +6,6 @@ export class Range { minInclusive: boolean maxInclusive: boolean - static from (s: string): Range { const r = new Range() r.minInclusive = s.startsWith('[') @@ -18,11 +17,23 @@ export class Range { } checkIncludes (n: number) { - if (this.hasMin() !== undefined && ((!this.minInclusive && this.min == n || (this.min > n)))) { - throw new Error(`Value must be ${this.minMessage()}.`) + if ( + this.hasMin() !== undefined && + ( + (this.min > n) || + (!this.minInclusive && this.min == n) + ) + ) { + throw new Error(this.minMessage()) } - if (this.hasMax() && ((!this.maxInclusive && this.max == n || (this.max < n)))) { - throw new Error(`Value must be ${this.maxMessage()}.`) + if ( + this.hasMax() && + ( + (this.max < n) || + (!this.maxInclusive && this.max == n) + ) + ) { + throw new Error(this.maxMessage()) } } diff --git a/ui/src/app/services/api/api.fixures.ts b/ui/src/app/services/api/api.fixures.ts index 84194e2ed..4d81d7617 100644 --- a/ui/src/app/services/api/api.fixures.ts +++ b/ui/src/app/services/api/api.fixures.ts @@ -979,7 +979,7 @@ export module Mock { 'warning': 'Chain will have to resync!', 'default': true, }, - 'objectList': { + 'object-list': { 'name': 'Object List', 'type': 'list', 'subtype': 'object', @@ -987,24 +987,24 @@ export module Mock { 'range': '[0,4]', 'default': [ { - 'firstName': 'Admin', - 'lastName': 'User', + 'first-name': 'Admin', + 'last-name': 'User', 'age': 40, }, { - 'firstName': 'Admin2', - 'lastName': 'User', + 'first-name': 'Admin2', + 'last-name': 'User', 'age': 40, }, ], // the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements. // it just so happens that ValueSpecObject's have the field { spec: ConfigSpec } - // see 'unionList' below for a different example. + // see 'union-list' below for a different example. 'spec': { - 'unique-by': 'lastName', - 'display-as': `I'm {{lastName}}, {{firstName}} {{lastName}}`, + 'unique-by': 'last-name', + 'display-as': `I'm {{last-name}}, {{first-name}} {{last-name}}`, 'spec': { - 'firstName': { + 'first-name': { 'name': 'First Name', 'type': 'string', 'description': 'User first name', @@ -1013,7 +1013,7 @@ export module Mock { 'masked': false, 'copyable': false, }, - 'lastName': { + 'last-name': { 'name': 'Last Name', 'type': 'string', 'description': 'User first name', @@ -1040,7 +1040,7 @@ export module Mock { }, }, }, - 'unionList': { + 'union-list': { 'name': 'Union List', 'type': 'list', 'subtype': 'union', @@ -1105,7 +1105,7 @@ export module Mock { 'unique-by': 'preference', }, }, - 'randomEnum': { + 'random-enum': { 'name': 'Random Enum', 'type': 'enum', 'value-names': { @@ -1124,18 +1124,18 @@ export module Mock { 'option3', ], }, - 'favoriteNumber': { + 'favorite-number': { 'name': 'Favorite Number', 'type': 'number', 'integral': false, 'description': 'Your favorite number of all time', 'warning': 'Once you set this number, it can never be changed without severe consequences.', - 'nullable': false, + 'nullable': true, 'default': 7, 'range': '(-100,100]', 'units': 'BTC', }, - 'unluckyNumbers': { + 'unlucky-numbers': { 'name': 'Unlucky Numbers', 'type': 'list', 'subtype': 'number', @@ -1276,7 +1276,7 @@ export module Mock { }, }, }, - 'bitcoinNode': { + 'bitcoin-node': { 'name': 'Bitcoin Node Settings', 'type': 'union', 'unique-by': null, @@ -1324,9 +1324,9 @@ export module Mock { 'description': 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', 'nullable': false, 'default': 8333, - 'range': '[0, 9999]', + 'range': '(0, 9998]', }, - 'favoriteSlogan': { + 'favorite-slogan': { 'name': 'Favorite Slogan', 'type': 'string', 'description': 'You most favorite slogan in the whole world, used for paying you.', @@ -1367,22 +1367,22 @@ export module Mock { // actual config config: { testnet: false, - objectList: [ + 'object-list': [ { - 'firstName': 'Admin', - 'lastName': 'User', + 'first-name': 'Admin', + 'last-name': 'User', 'age': 40, }, { - 'firstName': 'Admin2', - 'lastName': 'User', + 'first-name': 'Admin2', + 'last-name': 'User', 'age': 40, }, ], - unionList: undefined, - randomEnum: 'option1', - favoriteNumber: 8, - secondaryNumbers: undefined, + 'union-list': undefined, + 'random-enum': 'option1', + 'favorite-number': null, + 'secondary-numbers': undefined, rpcsettings: { laws: { law1: 'The first law', @@ -1395,7 +1395,7 @@ export module Mock { advanced: { notifications: ['email'], }, - bitcoinNode: undefined, + 'bitcoin-node': undefined, port: 5959, rpcallowip: undefined, rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'], @@ -1403,12 +1403,12 @@ export module Mock { } export const mockCupsDependentConfig = { - randomEnum: 'option1', + 'random-enum': 'option1', testnet: false, - favoriteNumber: 8, - secondaryNumbers: [13, 58, 20], - objectList: [], - unionList: [], + 'favorite-number': 8, + 'secondary-numbers': [13, 58, 20], + 'object-list': [], + 'union-list': [], rpcsettings: { laws: null, rpcpass: null, @@ -1418,7 +1418,7 @@ export module Mock { advanced: { notifications: [], }, - bitcoinNode: { type: 'internal' }, + 'bitcoin-Node': { type: 'internal' }, port: 5959, rpcallowip: [], rpcauth: ['matt: 8273gr8qwoidm1uid91jeh8y23gdio1kskmwejkdnm'], diff --git a/ui/src/app/services/api/api.types.ts b/ui/src/app/services/api/api.types.ts index 3aaaaa28f..ba4f692ed 100644 --- a/ui/src/app/services/api/api.types.ts +++ b/ui/src/app/services/api/api.types.ts @@ -175,11 +175,11 @@ export module RR { export type StopPackageReq = WithExpire<{ id: string }> // package.stop export type StopPackageRes = WithRevision - export type DryRemovePackageReq = RemovePackageReq // package.remove.dry - export type DryRemovePackageRes = Breakages + export type DryUninstallPackageReq = UninstallPackageReq // package.uninstall.dry + export type DryUninstallPackageRes = Breakages - export type RemovePackageReq = WithExpire<{ id: string }> // package.remove - export type RemovePackageRes = WithRevision + export type UninstallPackageReq = WithExpire<{ id: string }> // package.uninstall + export type UninstallPackageRes = WithRevision export type DryConfigureDependencyReq = { 'dependency-id': string, 'dependent-id': string } // package.dependency.configure.dry export type DryConfigureDependencyRes = object diff --git a/ui/src/app/services/api/embassy-api.service.ts b/ui/src/app/services/api/embassy-api.service.ts index 4e2060444..e5eb38f8b 100644 --- a/ui/src/app/services/api/embassy-api.service.ts +++ b/ui/src/app/services/api/embassy-api.service.ts @@ -171,11 +171,11 @@ export abstract class ApiService implements Source, Http { () => this.stopPackageRaw(params), )() - abstract dryRemovePackage (params: RR.DryRemovePackageReq): Promise + abstract dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise - protected abstract removePackageRaw (params: RR.RemovePackageReq): Promise - removePackage = (params: RR.RemovePackageReq) => this.syncResponse( - () => this.removePackageRaw(params), + protected abstract uninstallPackageRaw (params: RR.UninstallPackageReq): Promise + uninstallPackage = (params: RR.UninstallPackageReq) => this.syncResponse( + () => this.uninstallPackageRaw(params), )() abstract dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise diff --git a/ui/src/app/services/api/embassy-live-api.service.ts b/ui/src/app/services/api/embassy-live-api.service.ts index b0ecb2ae3..e9a35d4e6 100644 --- a/ui/src/app/services/api/embassy-live-api.service.ts +++ b/ui/src/app/services/api/embassy-live-api.service.ts @@ -260,12 +260,12 @@ export class LiveApiService extends ApiService { return this.http.rpcRequest({ method: 'package.stop', params }) } - async dryRemovePackage (params: RR.DryRemovePackageReq): Promise { - return this.http.rpcRequest({ method: 'package.remove.dry', params }) + async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise { + return this.http.rpcRequest({ method: 'package.uninstall.dry', params }) } - async removePackageRaw (params: RR.RemovePackageReq): Promise { - return this.http.rpcRequest({ method: 'package.remove', params }) + async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise { + return this.http.rpcRequest({ method: 'package.uninstall', params }) } async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise { diff --git a/ui/src/app/services/api/embassy-mock-api.service.ts b/ui/src/app/services/api/embassy-mock-api.service.ts index 0fad3ad4b..7f0b6ee9f 100644 --- a/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/ui/src/app/services/api/embassy-mock-api.service.ts @@ -452,12 +452,12 @@ export class MockApiService extends ApiService { return res } - async dryRemovePackage (params: RR.DryRemovePackageReq): Promise { + async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise { await pauseFor(2000) return { } } - async removePackageRaw (params: RR.RemovePackageReq): Promise { + async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise { await pauseFor(2000) const patch = [ { diff --git a/ui/src/app/services/form.service.ts b/ui/src/app/services/form.service.ts index d50cc5e27..1b8264a7e 100644 --- a/ui/src/app/services/form.service.ts +++ b/ui/src/app/services/form.service.ts @@ -153,12 +153,13 @@ function isFullUnion (spec: ValueSpecUnion | ListValueSpecUnion): spec is ValueS export function numberInRange (stringRange: string): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - if (!control.value) return null + const value = control.value + if (!value) return null try { - Range.from(stringRange).checkIncludes(control.value) + Range.from(stringRange).checkIncludes(value) return null } catch (e) { - return { numberNotInRange: { value: getRangeMessage(stringRange) } } + return { numberNotInRange: { value: `Number must be ${e.message}` } } } } } @@ -181,30 +182,15 @@ export function isInteger (): ValidatorFn { export function listInRange (stringRange: string): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { - const range = Range.from(stringRange) - const min = range.integralMin() - const max = range.integralMax() - const length = control.value.length - if ((min && length < min) || (max && length > max)) { - return { listNotInRange: { value: getRangeMessage(stringRange, true) } } - } else { + try { + Range.from(stringRange).checkIncludes(control.value.length) return null + } catch (e) { + return { numberNotInRange: { value: `List must be ${e.message}` } } } } } -function getRangeMessage (stringRange: string, isList = false): string { - const range = Range.from(stringRange) - const min = range.integralMin() - const max = range.integralMax() - const messageStart = isList ? 'List length must be ' : 'Must be ' - const minMessage = !!min ? `greater than ${min}` : '' - const and = !!min && !!max ? ' and ' : '' - const maxMessage = !!max ? `less than ${max}` : '' - - return `${messageStart} ${minMessage}${and}${maxMessage}` -} - export function listUnique (spec: ValueSpecList): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { for (let idx = 0; idx < control.value.length; idx++) { @@ -391,48 +377,41 @@ function isUnion (spec: any): spec is ListValueSpecUnion { return !!spec.tag } -const sampleUniqueBy: UniqueBy = { - all: [ - 'last name', - { any: [ - 'favorite color', - null, - ] }, - { any: [ - 'favorite color', - 'first name', - null, - ] }, - ], -} - -export function convertToNumberRecursive (configSpec: ConfigSpec, group: FormGroup) { +export function convertValuesRecursive (configSpec: ConfigSpec, group: FormGroup) { Object.entries(configSpec).forEach(([key, valueSpec]) => { if (valueSpec.type === 'number') { const control = group.get(key) - control.setValue(Number(control.value)) + control.setValue(control.value ? Number(control.value) : null) + } else if (valueSpec.type === 'string') { + const control = group.get(key) + if (!control.value) control.setValue(null) } else if (valueSpec.type === 'object') { - convertToNumberRecursive(valueSpec.spec, group.get(key) as FormGroup) + convertValuesRecursive(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) + convertValuesRecursive(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)) + control.setValue(control.value ? Number(control.value) : null) + }) + } else if (valueSpec.subtype === 'string') { + formArr.controls.forEach(control => { + if (!control.value) control.setValue(null) + control.setValue(control.value ? Number(control.value) : null) }) } else if (valueSpec.subtype === 'object') { formArr.controls.forEach((formGroup: FormGroup) => { const objectSpec = valueSpec.spec as ListValueSpecObject - convertToNumberRecursive(objectSpec.spec, formGroup) + convertValuesRecursive(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) + convertValuesRecursive(spec, formGroup) }) } }