implement select and multiselect for config

This commit is contained in:
Matt Hill
2023-03-25 19:31:06 -06:00
committed by Aiden McClelland
parent a657c332b1
commit 4a6a3da36c
21 changed files with 105 additions and 396 deletions

View File

@@ -49,7 +49,7 @@
"patch-db-client": "file: ../../../patch-db/client", "patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2", "pbkdf2": "^3.1.2",
"rxjs": "^7.5.6", "rxjs": "^7.5.6",
"start-sdk": "^0.4.0-lib0.alpha8", "start-sdk": "^0.4.0-lib0.alpha9",
"swiper": "^8.2.4", "swiper": "^8.2.4",
"ts-matches": "^5.2.1", "ts-matches": "^5.2.1",
"tslib": "^2.3.0", "tslib": "^2.3.0",
@@ -13771,9 +13771,9 @@
} }
}, },
"node_modules/start-sdk": { "node_modules/start-sdk": {
"version": "0.4.0-lib0.alpha8", "version": "0.4.0-lib0.alpha9",
"resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.alpha8.tgz", "resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.alpha9.tgz",
"integrity": "sha512-qErlv8ikV8nYqyCxxSN856dUwddGK5OOwTXk62IiJPxY3si03P1NQ3MnFch6Vx3NXiOmKeNNqw4/bj26TdUWRA==", "integrity": "sha512-F+ekxjVEKgNv7SU5XCe1rn7PqbKsbqCSS7pecMxcKQmbNlic4iyo3+lIS9JuPBSyTTjJlzJFzkdaxGwf4h83mg==",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@@ -74,7 +74,7 @@
"patch-db-client": "file: ../../../patch-db/client", "patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2", "pbkdf2": "^3.1.2",
"rxjs": "^7.5.6", "rxjs": "^7.5.6",
"start-sdk": "^0.4.0-lib0.alpha8", "start-sdk": "^0.4.0-lib0.alpha9",
"swiper": "^8.2.4", "swiper": "^8.2.4",
"ts-matches": "^5.2.1", "ts-matches": "^5.2.1",
"tslib": "^2.3.0", "tslib": "^2.3.0",

View File

@@ -5,7 +5,6 @@ import { IonicModule } from '@ionic/angular'
import { SharedPipesModule } from '@start9labs/shared' import { SharedPipesModule } from '@start9labs/shared'
import { TuiElasticContainerModule } from '@taiga-ui/kit' import { TuiElasticContainerModule } from '@taiga-ui/kit'
import { TuiExpandModule } from '@taiga-ui/core' import { TuiExpandModule } from '@taiga-ui/core'
import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module'
import { FormLabelComponent } from './form-label/form-label.component' import { FormLabelComponent } from './form-label/form-label.component'
import { FormObjectComponent } from './form-object/form-object.component' import { FormObjectComponent } from './form-object/form-object.component'
import { FormUnionComponent } from './form-union/form-union.component' import { FormUnionComponent } from './form-union/form-union.component'
@@ -13,15 +12,13 @@ import {
GetErrorPipe, GetErrorPipe,
ToWarningTextPipe, ToWarningTextPipe,
ToElementIdPipe, ToElementIdPipe,
ToEnumListDisplayPipe,
ToRangePipe, ToRangePipe,
} from './form-object.pipes' } from './form-object.pipes'
import { FormFileComponent } from './form-object/controls/form-file/form-file.component' import { FormFileComponent } from './form-object/controls/form-file/form-file.component'
import { FormInputComponent } from './form-object/controls/form-input/form-input.component' import { FormInputComponent } from './form-object/controls/form-input/form-input.component'
import { FormWarningDirective } from './form-warning.directive' import { FormWarningDirective } from './form-warning.directive'
import { FormSubformComponent } from './form-object/controls/form-subform/form-subform.component' import { FormSubformComponent } from './form-object/controls/form-subform/form-subform.component'
import { FormEnumComponent } from './form-object/controls/form-enum/form-enum.component' import { FormSelectComponent } from './form-object/controls/form-select/form-select.component'
import { FormValueComponent } from './form-object/controls/form-value/form-value.component'
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -30,15 +27,13 @@ import { FormValueComponent } from './form-object/controls/form-value/form-value
FormLabelComponent, FormLabelComponent,
ToWarningTextPipe, ToWarningTextPipe,
GetErrorPipe, GetErrorPipe,
ToEnumListDisplayPipe,
ToElementIdPipe, ToElementIdPipe,
ToRangePipe, ToRangePipe,
FormWarningDirective, FormWarningDirective,
FormFileComponent, FormFileComponent,
FormInputComponent, FormInputComponent,
FormSubformComponent, FormSubformComponent,
FormEnumComponent, FormSelectComponent,
FormValueComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,
@@ -46,7 +41,6 @@ import { FormValueComponent } from './form-object/controls/form-value/form-value
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
SharedPipesModule, SharedPipesModule,
EnumListPageModule,
TuiElasticContainerModule, TuiElasticContainerModule,
TuiExpandModule, TuiExpandModule,
], ],

View File

@@ -1,12 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core'
import { import { ValidationErrors } from '@angular/forms'
AbstractControl,
FormGroup,
UntypedFormArray,
ValidationErrors,
} from '@angular/forms'
import { IonicSafeString } from '@ionic/angular' import { IonicSafeString } from '@ionic/angular'
import { ListValueSpecOf } from 'start-sdk/types/config-types'
import { Range } from 'src/app/util/config-utilities' import { Range } from 'src/app/util/config-utilities'
import { getElementId } from './form-object/form-object.component' import { getElementId } from './form-object/form-object.component'
@@ -40,15 +34,6 @@ export class GetErrorPipe implements PipeTransform {
} }
} }
@Pipe({
name: 'toEnumListDisplay',
})
export class ToEnumListDisplayPipe implements PipeTransform {
transform(arr: string[], spec: ListValueSpecOf<'enum'>): string {
return arr.map((v: string) => spec['value-names'][v]).join(', ')
}
}
@Pipe({ @Pipe({
name: 'toWarningText', name: 'toWarningText',
}) })

View File

@@ -1,21 +0,0 @@
<form-label
class="label"
[data]="{
name: spec.name,
description: spec.description,
edited: control.dirty
}"
></form-label>
<ion-item button detail="false" color="dark" (click)="edit.emit()">
<ion-label class="list">
<h2>{{ control.value | toEnumListDisplay : $any(spec.spec) }}</h2>
</ion-label>
<ion-button slot="end" fill="clear" color="light">
<ion-icon slot="icon-only" name="chevron-down"></ion-icon>
</ion-button>
</ion-item>
<p class="error-message">
<span *ngIf="control.errors as errors">
{{ errors | getError }}
</span>
</p>

View File

@@ -1,15 +0,0 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'
import { AbstractControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
@Component({
selector: 'form-enum',
templateUrl: './form-enum.component.html',
styleUrls: ['./form-enum.component.scss'],
})
export class FormEnumComponent {
@Input() spec!: ValueSpecOf<'list'>
@Input() control!: AbstractControl
@Output() edit = new EventEmitter<void>()
}

View File

@@ -13,12 +13,12 @@
#warning="formWarning" #warning="formWarning"
slot="end" slot="end"
[formControl]="control" [formControl]="control"
(ionChange)="warning.onChange(name, spec, undefined, cancel)" (ionChange)="warning.onChange(name, spec, undefined, cancelBool)"
></ion-toggle> ></ion-toggle>
<!-- enum --> <!-- select -->
<!-- class enter-click disables the enter click on the modal behind the select --> <!-- adding class enter-click disables the enter click on the modal behind the select -->
<ion-select <ion-select
*ngIf="spec.type === 'enum'" *ngIf="spec.type === 'select' || spec.type === 'multiselect'"
[interfaceOptions]="{ [interfaceOptions]="{
header: spec.name, header: spec.name,
message: spec.warning | toWarningText, message: spec.warning | toWarningText,
@@ -26,11 +26,21 @@
}" }"
slot="end" slot="end"
placeholder="Select" placeholder="Select"
[multiple]="spec.type === 'multiselect'"
[formControl]="control" [formControl]="control"
[selectedText]="spec['value-names'][control.value]" [selectedText]="
spec.type === 'multiselect' && control.value?.length > 1
? '[' + control.value.length + ' selected]'
: spec['value-names'][control.value]
"
> >
<ion-select-option *ngFor="let option of spec.values" [value]="option"> <ion-select-option *ngFor="let option of spec.values" [value]="option">
{{ spec['value-names'][option] }} {{ spec['value-names'][option] }}
</ion-select-option> </ion-select-option>
</ion-select> </ion-select>
</ion-item> </ion-item>
<p class="error-message">
<span *ngIf="control.errors as errors">
{{ errors | getError }}
</span>
</p>

View File

@@ -1,11 +1,3 @@
.label {
margin: 16px 0 6px;
}
.list {
white-space: nowrap;
}
.error-message { .error-message {
margin-top: 2px; margin-top: 2px;
font-size: small; font-size: small;

View File

@@ -0,0 +1,16 @@
import { Component, Input } from '@angular/core'
import { FormControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
@Component({
selector: 'form-select',
templateUrl: './form-select.component.html',
styleUrls: ['./form-select.component.scss'],
})
export class FormSelectComponent {
@Input() spec!: ValueSpecOf<'boolean' | 'select' | 'multiselect'>
@Input() control!: FormControl
@Input() name!: string
cancelBool = () => this.control.setValue(!this.control.value)
}

View File

@@ -1,15 +0,0 @@
import { Component, Input } from '@angular/core'
import { FormControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
@Component({
selector: 'form-value',
templateUrl: './form-value.component.html',
})
export class FormValueComponent {
@Input() spec!: ValueSpecOf<'boolean' | 'enum'>
@Input() control!: FormControl
@Input() name!: string
cancel = () => this.control.setValue(!this.control.value)
}

View File

@@ -15,13 +15,17 @@
[control]="$any(entry.value)" [control]="$any(entry.value)"
(onInputChange)="handleInputChange()" (onInputChange)="handleInputChange()"
></form-input> ></form-input>
<!-- boolean or enum --> <!-- boolean, select or multiselect -->
<form-value <form-select
*ngIf="spec.type === 'boolean' || spec.type === 'enum'" *ngIf="
spec.type === 'boolean' ||
spec.type === 'select' ||
spec.type === 'multiselect'
"
[spec]="spec" [spec]="spec"
[name]="entry.key" [name]="entry.key"
[control]="$any(entry.value)" [control]="$any(entry.value)"
></form-value> ></form-select>
<!-- object --> <!-- object -->
<form-subform <form-subform
*ngIf="spec.type === 'object'" *ngIf="spec.type === 'object'"
@@ -46,8 +50,8 @@
[current]="current?.[entry.key]" [current]="current?.[entry.key]"
[original]="original?.[entry.key]" [original]="original?.[entry.key]"
></form-union> ></form-union>
<!-- list (not enum) --> <!-- list -->
<ng-container *ngIf="spec.type === 'list' && spec.subtype !== 'enum'"> <ng-container *ngIf="spec.type === 'list'">
<ng-container <ng-container
*ngIf="formGroup.get(entry.key) as formArr" *ngIf="formGroup.get(entry.key) as formArr"
[formArrayName]="entry.key" [formArrayName]="entry.key"
@@ -195,13 +199,6 @@
</div> </div>
</ng-container> </ng-container>
</ng-container> </ng-container>
<!-- list (enum) -->
<form-enum
*ngIf="spec.type === 'list' && spec.subtype === 'enum'"
[spec]="spec"
[control]="entry.value"
(edit)="presentModalEnumList(entry.key, $any(spec), entry.value.value)"
></form-enum>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ion-item-group> </ion-item-group>

View File

@@ -7,7 +7,7 @@ import {
inject, inject,
SimpleChanges, SimpleChanges,
} from '@angular/core' } from '@angular/core'
import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms' import { UntypedFormArray, UntypedFormGroup } from '@angular/forms'
import { AlertButton, AlertController, ModalController } from '@ionic/angular' import { AlertButton, AlertController, ModalController } from '@ionic/angular'
import { import {
InputSpec, InputSpec,
@@ -15,11 +15,9 @@ import {
ValueSpec, ValueSpec,
ValueSpecBoolean, ValueSpecBoolean,
ValueSpecList, ValueSpecList,
ValueSpecListOf,
ValueSpecUnion, ValueSpecUnion,
} from 'start-sdk/types/config-types' } from 'start-sdk/types/config-types'
import { FormService } from 'src/app/services/form.service' import { FormService } from 'src/app/services/form.service'
import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page'
import { THEME, pauseFor } from '@start9labs/shared' import { THEME, pauseFor } from '@start9labs/shared'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { DOCUMENT } from '@angular/common' import { DOCUMENT } from '@angular/common'
@@ -148,28 +146,6 @@ export class FormObjectComponent {
} }
} }
async presentModalEnumList(
key: string,
spec: ValueSpecListOf<'enum'>,
current: string[],
) {
const modal = await this.modalCtrl.create({
componentProps: {
key,
spec,
current,
},
component: EnumListPage,
})
modal.onWillDismiss<string[]>().then(({ data }) => {
if (!data) return
this.updateEnumList(key, current, data)
})
await modal.present()
}
async presentAlertChangeWarning<T extends ValueSpec>( async presentAlertChangeWarning<T extends ValueSpec>(
key: string, key: string,
spec: T extends ValueSpecUnion ? never : T, spec: T extends ValueSpecUnion ? never : T,
@@ -272,27 +248,6 @@ export class FormObjectComponent {
}) })
} }
private updateEnumList(key: string, current: string[], updated: string[]) {
const arr = this.formGroup.get(key) as FormArray
for (let i = current.length - 1; i >= 0; i--) {
if (!updated.includes(current[i])) {
arr.removeAt(i)
}
}
const listSpec = this.objectSpec[key] as ValueSpecList
updated.forEach(val => {
if (!current.includes(val)) {
const newItem = this.formService.getListItem(listSpec, val)!
arr.insert(arr.length, newItem)
}
})
arr.markAsDirty()
}
asIsOrder() { asIsOrder() {
return 0 return 0
} }

View File

@@ -1,16 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { EnumListPage } from './enum-list.page'
import { FormsModule } from '@angular/forms'
@NgModule({
declarations: [EnumListPage],
imports: [
CommonModule,
IonicModule,
FormsModule,
],
exports: [EnumListPage],
})
export class EnumListPageModule { }

View File

@@ -1,45 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-title> {{ spec.name }} </ion-title>
<ion-buttons slot="end">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item-group>
<ion-item-divider style="padding-bottom: 6px">
<ion-buttons slot="end">
<ion-button fill="clear" (click)="toggleSelectAll()">
<b>{{ selectAll ? 'Select All' : 'Deselect All' }}</b>
</ion-button>
</ion-buttons>
</ion-item-divider>
<ion-item *ngFor="let option of options | keyvalue : asIsOrder">
<ion-label>{{ spec.spec['value-names'][option.key] }}</ion-label>
<ion-checkbox
slot="end"
[(ngModel)]="option.value"
(click)="toggleSelected(option.key)"
></ion-checkbox>
</ion-item>
</ion-item-group>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-buttons slot="end" class="ion-padding-end">
<ion-button
fill="solid"
color="primary"
(click)="save()"
class="enter-click btn-128"
>
Done
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>

View File

@@ -1,50 +0,0 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { ValueSpecListOf } from 'start-sdk/types/config-types'
@Component({
selector: 'enum-list',
templateUrl: './enum-list.page.html',
styleUrls: ['./enum-list.page.scss'],
})
export class EnumListPage {
@Input() key!: string
@Input() spec!: ValueSpecListOf<'enum'>
@Input() current: string[] = []
options: { [option: string]: boolean } = {}
selectAll = false
constructor(private readonly modalCtrl: ModalController) {}
ngOnInit() {
for (let val of this.spec.spec.values || []) {
this.options[val] = this.current.includes(val)
}
// if none are selected, set selectAll to true
this.selectAll = Object.values(this.options).some(k => !k)
}
dismiss() {
this.modalCtrl.dismiss()
}
save() {
this.modalCtrl.dismiss(
Object.keys(this.options).filter(key => this.options[key]),
)
}
toggleSelectAll() {
Object.keys(this.options).forEach(k => (this.options[k] = this.selectAll))
this.selectAll = !this.selectAll
}
toggleSelected(key: string) {
this.options[key] = !this.options[key]
}
asIsOrder() {
return 0
}
}

View File

@@ -253,8 +253,8 @@ const SAMPLE_CONFIG: InputSpec = {
default: true, default: true,
warning: null, warning: null,
}, },
'sample-enum': { 'sample-select': {
type: 'enum', type: 'multiselect',
name: 'Example Enum Select', name: 'Example Enum Select',
values: ['red', 'blue', 'green'], values: ['red', 'blue', 'green'],
'value-names': { 'value-names': {
@@ -263,8 +263,9 @@ const SAMPLE_CONFIG: InputSpec = {
green: 'Green', green: 'Green',
}, },
// optional // optional
warning: 'Example warning to display when changing this enum value.', warning: 'Example warning to display when changing this select value.',
description: 'Example description for enum select', description: 'Example description for select select',
default: 'red', range: '[0, 2)',
default: ['red'],
}, },
} }

View File

@@ -110,7 +110,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
warning: null, warning: null,
}, },
license: { license: {
type: 'enum', type: 'select',
name: 'License', name: 'License',
warning: null, warning: null,
values: [ values: [
@@ -135,7 +135,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
'the-unlicense': 'The Unlicense', 'the-unlicense': 'The Unlicense',
custom: 'Custom', custom: 'Custom',
}, },
description: 'Example description for enum select', description: 'Example description for select',
default: 'mit', default: 'mit',
}, },
'wrapper-repo': { 'wrapper-repo': {

View File

@@ -963,7 +963,7 @@ export module Mock {
}, },
'favorite-flower': { 'favorite-flower': {
name: 'Favorite Flower', name: 'Favorite Flower',
type: 'enum', type: 'select',
description: 'Select your favorite flower', description: 'Select your favorite flower',
warning: null, warning: null,
'value-names': { 'value-names': {
@@ -989,19 +989,34 @@ export module Mock {
'unique-by': 'preference', 'unique-by': 'preference',
}, },
}, },
'random-enum': { 'random-select': {
name: 'Random Enum', name: 'Random Select',
type: 'enum', type: 'select',
'value-names': { 'value-names': {
null: 'Null', hello: 'Hello',
option1: 'One 1', goodbye: 'Goodbye',
option2: 'Two 2', sup: 'Sup',
option3: 'Three 3',
}, },
default: 'null', default: 'sup',
description: 'This is not even real.', description: 'This is not even real.',
warning: 'Be careful changing this!', warning: 'Be careful changing this!',
values: ['null', 'option1', 'option2', 'option3'], values: ['hello', 'goodbye', 'sup'],
},
notifications: {
name: 'Notification Preferences',
type: 'multiselect',
description: 'how you want to be notified',
warning: null,
range: '(1,3]',
'value-names': {
email: 'EEEEmail',
text: 'Texxxt',
call: 'Ccccall',
push: 'PuuuusH',
webhook: 'WebHooookkeee',
},
values: ['email', 'text', 'call', 'push', 'webhook'],
default: ['email'],
}, },
'favorite-number': { 'favorite-number': {
name: 'Favorite Number', name: 'Favorite Number',
@@ -1292,25 +1307,6 @@ export module Mock {
description: 'Advanced settings', description: 'Advanced settings',
warning: null, warning: null,
spec: { spec: {
notifications: {
name: 'Notification Preferences',
type: 'list',
subtype: 'enum',
description: 'how you want to be notified',
warning: null,
range: '[1,3]',
default: ['email'],
spec: {
'value-names': {
email: 'EEEEmail',
text: 'Texxxt',
call: 'Ccccall',
push: 'PuuuusH',
webhook: 'WebHooookkeee',
},
values: ['email', 'text', 'call', 'push', 'webhook'],
},
},
rpcsettings: { rpcsettings: {
name: 'RPC Settings', name: 'RPC Settings',
type: 'object', type: 'object',
@@ -1525,7 +1521,7 @@ export module Mock {
}, },
], ],
'union-list': undefined, 'union-list': undefined,
'random-enum': 'option2', 'random-select': ['goodbye'],
'favorite-number': 0, 'favorite-number': 0,
rpcsettings: { rpcsettings: {
laws: { laws: {

View File

@@ -17,7 +17,8 @@ import {
ListValueSpecUnion, ListValueSpecUnion,
UniqueBy, UniqueBy,
ValueSpec, ValueSpec,
ValueSpecEnum, ValueSpecSelect,
ValueSpecMultiselect,
ValueSpecFile, ValueSpecFile,
ValueSpecList, ValueSpecList,
ValueSpecNumber, ValueSpecNumber,
@@ -49,8 +50,8 @@ export class FormService {
const { variants, tag } = spec const { variants, tag } = spec
const { name, description, warning, 'variant-names': variantNames } = tag const { name, description, warning, 'variant-names': variantNames } = tag
const enumSpec: ValueSpecEnum = { const enumSpec: ValueSpecSelect = {
type: 'enum', type: 'select',
name, name,
description, description,
warning, warning,
@@ -71,8 +72,6 @@ export class FormService {
return this.formBuilder.control(entry, listItemValidators) return this.formBuilder.control(entry, listItemValidators)
} else if (isValueSpecListOf(spec, 'number')) { } else if (isValueSpecListOf(spec, 'number')) {
return this.formBuilder.control(entry, listItemValidators) return this.formBuilder.control(entry, listItemValidators)
} else if (isValueSpecListOf(spec, 'enum')) {
return this.formBuilder.control(entry)
} else if (isValueSpecListOf(spec, 'object')) { } else if (isValueSpecListOf(spec, 'object')) {
return this.getFormGroup(spec.spec.spec, listItemValidators, entry) return this.getFormGroup(spec.spec.spec, listItemValidators, entry)
} else if (isValueSpecListOf(spec, 'union')) { } else if (isValueSpecListOf(spec, 'union')) {
@@ -99,7 +98,6 @@ export class FormService {
spec: ValueSpec, spec: ValueSpec,
currentValue?: any, currentValue?: any,
): UntypedFormGroup | UntypedFormArray | UntypedFormControl { ): UntypedFormGroup | UntypedFormArray | UntypedFormControl {
let validators: ValidatorFn[]
let value: any let value: any
switch (spec.type) { switch (spec.type) {
case 'string': case 'string':
@@ -140,9 +138,12 @@ export class FormService {
isValid ? currentValue : undefined, isValid ? currentValue : undefined,
) )
case 'boolean': case 'boolean':
case 'enum': case 'select':
value = currentValue === undefined ? spec.default : currentValue value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value) return this.formBuilder.control(value)
case 'multiselect':
value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value, multiselectValidators(spec))
default: default:
return this.formBuilder.control(null) return this.formBuilder.control(null)
} }
@@ -193,17 +194,16 @@ function numberValidators(
return validators return validators
} }
function multiselectValidators(spec: ValueSpecMultiselect): ValidatorFn[] {
const validators: ValidatorFn[] = []
validators.push(listInRange(spec.range))
return validators
}
function listValidators(spec: ValueSpecList): ValidatorFn[] { function listValidators(spec: ValueSpecList): ValidatorFn[] {
const validators: ValidatorFn[] = [] const validators: ValidatorFn[] = []
validators.push(listInRange(spec.range)) validators.push(listInRange(spec.range))
validators.push(listItemIssue()) validators.push(listItemIssue())
if (!isValueSpecListOf(spec, 'enum')) {
validators.push(listUnique(spec))
}
return validators return validators
} }
@@ -314,7 +314,6 @@ function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean {
switch (spec.subtype) { switch (spec.subtype) {
case 'string': case 'string':
case 'number': case 'number':
case 'enum':
return val1 == val2 return val1 == val2
case 'object': case 'object':
const obj: ListValueSpecObject = spec.spec as any const obj: ListValueSpecObject = spec.spec as any
@@ -334,7 +333,7 @@ function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean {
case 'string': case 'string':
case 'number': case 'number':
case 'boolean': case 'boolean':
case 'enum': case 'select':
return val1 == val2 return val1 == val2
case 'object': case 'object':
// TODO: 'unique-by' does not exist on ValueSpecObject, fix types // TODO: 'unique-by' does not exist on ValueSpecObject, fix types

View File

@@ -1,4 +1,4 @@
import { ValueSpec, DefaultString } from 'start-sdk/types/config-types' import { DefaultString } from 'start-sdk/types/config-types'
export class Range { export class Range {
min?: number min?: number
@@ -31,95 +31,21 @@ export class Range {
} }
} }
hasMin(): this is Range & { min: number } { private hasMin(): this is Range & { min: number } {
return this.min !== undefined return this.min !== undefined
} }
hasMax(): this is Range & { max: number } { private hasMax(): this is Range & { max: number } {
return this.max !== undefined return this.max !== undefined
} }
minMessage(): string { private minMessage(): string {
return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}` return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}`
} }
maxMessage(): string { private maxMessage(): string {
return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}` return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}`
} }
description(): string {
let message = 'Value can be any number.'
if (this.hasMin() || this.hasMax()) {
message = 'Value must be'
}
if (this.hasMin() && this.hasMax()) {
message = `${message} ${this.minMessage()} AND ${this.maxMessage()}.`
} else if (this.hasMin() && !this.hasMax()) {
message = `${message} ${this.minMessage()}.`
} else if (!this.hasMin() && this.hasMax()) {
message = `${message} ${this.maxMessage()}.`
}
return message
}
integralMin(): number | undefined {
if (this.min) {
const ceil = Math.ceil(this.min)
if (this.minInclusive) {
return ceil
} else {
if (ceil === this.min) {
return ceil + 1
} else {
return ceil
}
}
}
}
integralMax(): number | undefined {
if (this.max) {
const floor = Math.floor(this.max)
if (this.maxInclusive) {
return floor
} else {
if (floor === this.max) {
return floor - 1
} else {
return floor
}
}
}
}
}
export function getDefaultDescription(spec: ValueSpec): string {
let toReturn: string | undefined
switch (spec.type) {
case 'string':
if (typeof spec.default === 'string') {
toReturn = spec.default
} else if (typeof spec.default === 'object') {
toReturn = 'random'
}
break
case 'number':
if (typeof spec.default === 'number') {
toReturn = String(spec.default)
}
break
case 'boolean':
toReturn = spec.default === true ? 'True' : 'False'
break
case 'enum':
toReturn = spec['value-names'][spec.default]
break
}
return toReturn || ''
} }
export function getDefaultString(defaultSpec: DefaultString): string { export function getDefaultString(defaultSpec: DefaultString): string {
@@ -136,7 +62,7 @@ export function getDefaultString(defaultSpec: DefaultString): string {
} }
// a,g,h,A-Z,,,,- // a,g,h,A-Z,,,,-
export function getRandomCharInSet(charset: string): string { function getRandomCharInSet(charset: string): string {
const set = stringToCharSet(charset) const set = stringToCharSet(charset)
let charIdx = Math.floor(Math.random() * set.len) let charIdx = Math.floor(Math.random() * set.len)
for (let range of set.ranges) { for (let range of set.ranges) {