update form to latest sdk

This commit is contained in:
Matt Hill
2023-04-21 09:21:35 -06:00
committed by Aiden McClelland
parent f83ae27352
commit 94cdaf5314
31 changed files with 499 additions and 310 deletions

View File

@@ -49,7 +49,7 @@
"patch-db-client": "file: ../../../patch-db/client",
"pbkdf2": "^3.1.2",
"rxjs": "^7.5.6",
"start-sdk": "^0.4.0-lib0.charlie20",
"start-sdk": "^0.4.0-lib0.charlie33",
"swiper": "^8.2.4",
"ts-matches": "^5.2.1",
"tslib": "^2.3.0",
@@ -6220,7 +6220,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13733,11 +13732,12 @@
}
},
"node_modules/start-sdk": {
"version": "0.4.0-lib0.charlie20",
"resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.charlie20.tgz",
"integrity": "sha512-TzvD5rfDnHDqhv/R1bJG+Fg8zvSCJZ3QhmHrf868ZM54ABTh/5Qn7TRXdS0b5CgrhHy3/uwZi/5G3SyWNLrDoQ==",
"version": "0.4.0-lib0.charlie33",
"resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.charlie33.tgz",
"integrity": "sha512-Lx3QAuRCZTA6zXdjsKN3LjCTY1NQgEHY9uOyxKSTnWtaVXdOSp1GqiZYs3c7/ZStPzzTZ19kSnCcVBE125lUHA==",
"dependencies": {
"@iarna/toml": "^2.2.5",
"deepmerge": "^4.3.1",
"lodash": "^4.17.21",
"ts-matches": "^5.4.1",
"yaml": "^2.2.1"

View File

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

View File

@@ -276,53 +276,57 @@ export class BackupDrivesStatusComponent {
const CifsSpec: InputSpec = {
hostname: {
type: 'string',
type: 'text',
name: 'Hostname',
description:
'The hostname of your target device on the Local Area Network.',
inputmode: 'text',
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
pattern: '^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$',
patternDescription: `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
minLength: null,
maxLength: null,
patterns: [],
required: true,
masked: false,
default: null,
warning: null,
},
path: {
type: 'string',
type: 'text',
name: 'Path',
description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`,
inputmode: 'text',
placeholder: 'e.g. my-shared-folder or /Desktop/my-folder',
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
required: true,
masked: false,
default: null,
warning: null,
},
username: {
type: 'string',
type: 'text',
name: 'Username',
description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`,
inputmode: 'text',
minLength: null,
maxLength: null,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
required: true,
masked: false,
default: null,
warning: null,
},
password: {
type: 'string',
type: 'text',
name: 'Password',
description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`,
inputmode: 'text',
placeholder: null,
pattern: null,
patternDescription: null,
minLength: null,
maxLength: null,
patterns: [],
required: false,
masked: true,
default: null,

View File

@@ -24,9 +24,12 @@
type="text"
class="input"
[class.input_redacted]="
spec.type === 'string' && control.value && spec.masked && !unmasked
spec.type === 'text' && control.value && spec.masked && !unmasked
"
[inputmode]="spec.type === 'string' ? spec.inputmode : 'tel'"
[inputmode]="spec.type === 'text' ? spec.inputmode : 'tel'"
[minlength]="spec.type === 'number' ? null : spec.minLength"
[maxlength]="spec.type === 'number' ? null : spec.maxLength"
[step]="spec.type === 'number' ? spec.step : null"
[placeholder]="spec.placeholder"
[formControl]="control"
(ionFocus)="warning.onChange(name, spec)"
@@ -34,7 +37,7 @@
></ion-input>
</ng-template>
<ion-button
*ngIf="spec.type === 'string' && spec.masked"
*ngIf="spec.type === 'text' && spec.masked"
slot="end"
fill="clear"
color="light"

View File

@@ -10,7 +10,7 @@ import { THEME } from '@start9labs/shared'
})
export class FormInputComponent {
@Input() name!: string
@Input() spec!: ValueSpecOf<'string' | 'textarea' | 'number'>
@Input() spec!: ValueSpecOf<'text' | 'textarea' | 'number'>
@Input() control!: FormControl
@Output() onInputChange = new EventEmitter<void>()

View File

@@ -8,7 +8,7 @@
></form-label>
<!-- boolean -->
<ion-toggle
*ngIf="spec.type === 'boolean'"
*ngIf="spec.type === 'toggle'"
formWarning
#warning="formWarning"
slot="end"

View File

@@ -8,7 +8,7 @@ import { ValueSpecOf } from 'start-sdk/lib/config/configTypes'
styleUrls: ['./form-select.component.scss'],
})
export class FormSelectComponent {
@Input() spec!: ValueSpecOf<'boolean' | 'select' | 'multiselect'>
@Input() spec!: ValueSpecOf<'toggle' | 'select' | 'multiselect'>
@Input() control!: FormControl
@Input() name!: string

View File

@@ -10,7 +10,7 @@
<!-- string or number -->
<form-input
*ngIf="
spec.type === 'string' ||
spec.type === 'text' ||
spec.type === 'textarea' ||
spec.type === 'number'
"
@@ -22,7 +22,7 @@
<!-- boolean, select or multiselect -->
<form-select
*ngIf="
spec.type === 'boolean' ||
spec.type === 'toggle' ||
spec.type === 'select' ||
spec.type === 'multiselect'
"
@@ -67,7 +67,7 @@
name: spec.name,
description: spec.description || null,
edited: entry.value.dirty,
required: !!(spec.range | toRange).min
required: !!spec.minLength
}"
></form-label>
<ion-button
@@ -153,9 +153,7 @@
</ng-container>
<!-- string or number -->
<div
*ngIf="
spec.spec.type === 'string' || spec.spec.type === 'number'
"
*ngIf="spec.spec.type === 'text' || spec.spec.type === 'number'"
[id]="objectId | toElementId : entry.key : i"
>
<ion-item
@@ -164,7 +162,7 @@
<ion-input
type="text"
[inputmode]="
spec.spec.type === 'string' ? spec.spec.inputmode : 'tel'
spec.spec.type === 'text' ? spec.spec.inputmode : 'tel'
"
[placeholder]="
$any(spec.spec).placeholder || 'Enter ' + spec.name

View File

@@ -13,7 +13,7 @@ import {
InputSpec,
ListValueSpecOf,
ValueSpec,
ValueSpecBoolean,
ValueSpecToggle,
ValueSpecList,
ValueSpecUnion,
} from 'start-sdk/lib/config/configTypes'
@@ -135,7 +135,7 @@ export class FormObjectComponent {
})
}
handleBooleanChange(key: string, spec: ValueSpecBoolean) {
handleBooleanChange(key: string, spec: ValueSpecToggle) {
if (spec.warning) {
const current = this.formGroup.get(key)?.value
const cancelFn = () => this.formGroup.get(key)?.setValue(!current)

View File

@@ -1,10 +0,0 @@
import { Component } from '@angular/core'
import { ValueSpecBoolean } from 'start-sdk/lib/config/configTypes'
import { Control } from '../control'
@Component({
selector: 'form-boolean',
templateUrl: './form-boolean.component.html',
styleUrls: ['./form-boolean.component.scss'],
})
export class FormBooleanComponent extends Control<ValueSpecBoolean, boolean> {}

View File

@@ -1,8 +1,8 @@
<ng-container [ngSwitch]="spec.type">
<form-string *ngSwitchCase="'string'"></form-string>
<form-text *ngSwitchCase="'text'"></form-text>
<form-number *ngSwitchCase="'number'"></form-number>
<form-text *ngSwitchCase="'textarea'"></form-text>
<form-boolean *ngSwitchCase="'boolean'"></form-boolean>
<form-toggle *ngSwitchCase="'toggle'"></form-toggle>
<form-select *ngSwitchCase="'select'"></form-select>
<form-multiselect *ngSwitchCase="'multiselect'"></form-multiselect>
<form-file *ngSwitchCase="'file'"></form-file>

View File

@@ -14,7 +14,7 @@ import {
} from '@taiga-ui/core'
import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit'
import { filter, takeUntil } from 'rxjs'
import { ValueSpec, ValueSpecString } from 'start-sdk/lib/config/configTypes'
import { ValueSpec, ValueSpecText } from 'start-sdk/lib/config/configTypes'
import { ERRORS } from '../form-group/form-group.component'
@Component({
@@ -26,9 +26,9 @@ import { ERRORS } from '../form-group/form-group.component'
{
provide: TUI_VALIDATION_ERRORS,
deps: [FormControlComponent],
useFactory: (control: FormControlComponent<ValueSpecString, string>) => ({
useFactory: (control: FormControlComponent<ValueSpecText, string>) => ({
required: 'Required',
pattern: () => control.spec.patternDescription,
patterns: () => control.spec.patterns.map(p => p.regex), // @TODO Alex
}),
},
],

View File

@@ -1,10 +1,12 @@
<!-- @TODO Alex look at "attr.step", is this correct? -->
<tui-input-number
[tuiHintContent]="spec.description"
[tuiTextfieldPostfix]="spec.units || ''"
[precision]="Infinity"
[decimal]="spec.integral ? 'never' : 'not-zero'"
[decimal]="spec.integer ? 'never' : 'not-zero'"
[min]="min"
[max]="max"
[attr.step]="spec.step"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>

View File

@@ -1,6 +1,5 @@
import { Component } from '@angular/core'
import { ValueSpecNumber } from 'start-sdk/lib/config/configTypes'
import { Range } from 'src/app/util/config-utilities'
import { Control } from '../control'
@Component({
@@ -9,17 +8,14 @@ import { Control } from '../control'
})
export class FormNumberComponent extends Control<ValueSpecNumber, number> {
protected readonly Infinity = Infinity
private range = Range.from(this.spec.range)
get min(): number {
const min = this.range.min || -Infinity
return this.range.minInclusive || !this.spec.integral ? min : min + 1
if (typeof this.spec.min !== 'number') return -Infinity
return this.spec.min
}
get max(): number {
const max = this.range.max || Infinity
return this.range.maxInclusive || !this.spec.integral ? max : max - 1
if (typeof this.spec.max !== 'number') return Infinity
return this.spec.max
}
}

View File

@@ -1,27 +0,0 @@
<tui-input
[tuiTextfieldCustomContent]="spec.masked ? toggle : ''"
[tuiHintContent]="spec.description"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
<span *ngIf="spec.required">*</span>
<input
tuiTextfield
[class.masked]="spec.masked && masked"
[placeholder]="spec.placeholder || ''"
[attr.inputmode]="spec.inputmode"
/>
</tui-input>
<ng-template #toggle>
<button
tuiIconButton
type="button"
appearance="icon"
title="Toggle masking"
size="xs"
class="toggle"
[icon]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
(click)="masked = !masked"
></button>
</ng-template>

View File

@@ -1,12 +0,0 @@
import { Component } from '@angular/core'
import { ValueSpecString } from 'start-sdk/lib/config/configTypes'
import { Control } from '../control'
@Component({
selector: 'form-string',
templateUrl: './form-string.component.html',
styleUrls: ['./form-string.component.scss'],
})
export class FormStringComponent extends Control<ValueSpecString, string> {
masked = true
}

View File

@@ -1,11 +1,29 @@
<tui-text-area
<tui-input
[tuiTextfieldCustomContent]="spec.masked ? toggle : ''"
[tuiHintContent]="spec.description"
[expandable]="true"
[rows]="6"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
<span *ngIf="spec.required">*</span>
<textarea tuiTextfield [placeholder]="spec.placeholder || ''"></textarea>
</tui-text-area>
<input
tuiTextfield
[class.masked]="spec.masked && masked"
[placeholder]="spec.placeholder || ''"
[minLength]="spec.minLength"
[maxLength]="spec.maxLength"
[attr.inputmode]="spec.inputmode"
/>
</tui-input>
<ng-template #toggle>
<button
tuiIconButton
type="button"
appearance="icon"
title="Toggle masking"
size="xs"
class="toggle"
[icon]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
(click)="masked = !masked"
></button>
</ng-template>

View File

@@ -1,9 +1,12 @@
import { Component } from '@angular/core'
import { ValueSpecTextarea } from 'start-sdk/lib/config/configTypes'
import { ValueSpecText } from 'start-sdk/lib/config/configTypes'
import { Control } from '../control'
@Component({
selector: 'form-text',
templateUrl: './form-text.component.html',
styleUrls: ['./form-text.component.scss'],
})
export class FormTextComponent extends Control<ValueSpecTextarea, string> {}
export class FormTextComponent extends Control<ValueSpecText, string> {
masked = true
}

View File

@@ -0,0 +1,11 @@
<tui-text-area
[tuiHintContent]="spec.description"
[expandable]="true"
[rows]="6"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
<span *ngIf="spec.required">*</span>
<textarea tuiTextfield [placeholder]="spec.placeholder || ''"></textarea>
</tui-text-area>

View File

@@ -0,0 +1,9 @@
import { Component } from '@angular/core'
import { ValueSpecTextarea } from 'start-sdk/lib/config/configTypes'
import { Control } from '../control'
@Component({
selector: 'form-textarea',
templateUrl: './form-textarea.component.html',
})
export class FormTextareaComponent extends Control<ValueSpecTextarea, string> {}

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core'
import { ValueSpecToggle } from 'start-sdk/lib/config/configTypes'
import { Control } from '../control'
@Component({
selector: 'form-toggle',
templateUrl: './form-toggle.component.html',
styleUrls: ['./form-toggle.component.scss'],
})
export class FormToggleComponent extends Control<ValueSpecToggle, boolean> {}

View File

@@ -27,9 +27,9 @@ import {
} from '@taiga-ui/kit'
import { FormGroupComponent } from './form-group/form-group.component'
import { FormStringComponent } from './form-string/form-string.component'
import { FormBooleanComponent } from './form-boolean/form-boolean.component'
import { FormTextComponent } from './form-text/form-text.component'
import { FormToggleComponent } from './form-toggle/form-toggle.component'
import { FormTextareaComponent } from './form-textarea/form-textarea.component'
import { FormNumberComponent } from './form-number/form-number.component'
import { FormSelectComponent } from './form-select/form-select.component'
import { FormFileComponent } from './form-file/form-file.component'
@@ -70,9 +70,9 @@ import { ControlDirective } from './control.directive'
declarations: [
FormGroupComponent,
FormControlComponent,
FormStringComponent,
FormBooleanComponent,
FormTextComponent,
FormToggleComponent,
FormTextareaComponent,
FormNumberComponent,
FormSelectComponent,
FormMultiselectComponent,

View File

@@ -270,14 +270,20 @@ function getMarketplaceValueSpec(): ValueSpecObject {
warning: null,
spec: {
url: {
type: 'string',
type: 'text',
name: 'URL',
description: 'A fully-qualified URL of the custom registry',
inputmode: 'url',
required: true,
masked: false,
pattern: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`,
patternDescription: 'Must be a valid URL',
minLength: null,
maxLength: null,
patterns: [
{
regex: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`,
description: 'Must be a valid URL',
},
],
placeholder: 'e.g. https://example.org',
default: null,
warning: null,

View File

@@ -219,7 +219,7 @@ const SAMPLE_INSTUCTIONS = `# Create Instructions using Markdown! :)`
const SAMPLE_CONFIG: InputSpec = {
'sample-string': {
type: 'string',
type: 'text',
name: 'Example String Input',
inputmode: 'text',
required: true,
@@ -227,8 +227,14 @@ const SAMPLE_CONFIG: InputSpec = {
// optional
description: 'Example description for required string input.',
placeholder: 'Enter string value',
pattern: '^[a-zA-Z0-9! _]+$',
patternDescription: 'Must be alphanumeric (may contain underscore).',
patterns: [
{
regex: '^[a-zA-Z0-9! _]+$',
description: 'Must be alphanumeric (may contain underscore).',
},
],
minLength: null,
maxLength: null,
default: null,
warning: null,
},
@@ -236,8 +242,10 @@ const SAMPLE_CONFIG: InputSpec = {
type: 'number',
name: 'Example Number Input',
required: true,
range: '[5,1000000]',
integral: true,
min: 5,
max: 1000000,
step: '5',
integer: true,
// optional
warning: 'Example warning to display when changing this number value.',
units: 'ms',
@@ -246,7 +254,7 @@ const SAMPLE_CONFIG: InputSpec = {
default: null,
},
'sample-boolean': {
type: 'boolean',
type: 'toggle',
name: 'Example Boolean Toggle',
// optional
description: 'Example description for boolean toggle',
@@ -264,7 +272,8 @@ const SAMPLE_CONFIG: InputSpec = {
// optional
warning: 'Example warning to display when changing this select value.',
description: 'Example description for select select',
range: '[0, 2)',
minLength: null,
maxLength: 2,
default: ['red'],
},
}

View File

@@ -21,33 +21,40 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
const basicInfo = devData['basic-info']
return {
id: {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'ID',
description: 'The package identifier used by the OS',
placeholder: 'e.g. bitcoind',
required: true,
masked: false,
pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$',
patternDescription: 'Must be kebab case',
minLength: null,
maxLength: null,
patterns: [
{
regex: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$',
description: 'Must be kebab case',
},
],
default: basicInfo?.id || '',
warning: null,
},
title: {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Service Name',
description: 'A human readable service title',
placeholder: 'e.g. Bitcoin Core',
required: true,
masked: false,
pattern: null,
patternDescription: null,
minLength: null,
maxLength: null,
patterns: [],
default: basicInfo ? basicInfo.title : devData.name,
warning: null,
},
'service-version-number': {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Service Version',
description:
@@ -55,8 +62,14 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
placeholder: 'e.g. 0.1.2.3',
required: true,
masked: false,
pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$',
patternDescription: 'Must be valid Emver version',
minLength: null,
maxLength: null,
patterns: [
{
regex: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$',
description: 'Must be valid Emver version',
},
],
default: basicInfo?.['service-version-number'] || '',
warning: null,
},
@@ -67,7 +80,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
warning: null,
spec: {
short: {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Short Description',
description:
@@ -76,8 +89,9 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
required: true,
masked: false,
default: basicInfo?.description?.short || '',
pattern: '^.{1,320}$',
patternDescription: 'Must be shorter than 320 characters',
minLength: null,
maxLength: 320,
patterns: [],
warning: null,
},
long: {
@@ -85,22 +99,25 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
name: 'Long Description',
description: `This description will display with additional details in the service's individual marketplace page`,
placeholder: null,
minLength: 20,
maxLength: 1000,
required: true,
warning: null,
},
},
},
'release-notes': {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Release Notes',
description:
'Markdown supported release notes for this version of this service.',
placeholder: 'e.g. Markdown _release notes_ for **Bitcoin Core**',
required: true,
minLength: null,
maxLength: null,
masked: false,
pattern: null,
patternDescription: null,
patterns: [],
default: basicInfo?.['release-notes'] || '',
warning: null,
},
@@ -124,53 +141,57 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
default: 'mit',
},
'wrapper-repo': {
type: 'string',
type: 'text',
inputmode: 'url',
name: 'Wrapper Repo',
description:
'The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), any scripts necessary for configuration, backups, actions, or health checks',
placeholder: 'e.g. www.github.com/example',
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
required: true,
masked: false,
default: basicInfo?.['wrapper-repo'] || '',
warning: null,
},
'upstream-repo': {
type: 'string',
type: 'text',
inputmode: 'url',
name: 'Upstream Repo',
description: 'The original project repository URL',
placeholder: 'e.g. www.github.com/example',
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
required: false,
masked: false,
default: basicInfo?.['upstream-repo'] || '',
warning: null,
},
'support-site': {
type: 'string',
type: 'text',
inputmode: 'url',
name: 'Support Site',
description: 'URL to the support site / channel for the project',
placeholder: 'e.g. start9.com/support',
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
required: false,
masked: false,
default: basicInfo?.['support-site'] || '',
warning: null,
},
'marketing-site': {
type: 'string',
type: 'text',
inputmode: 'url',
name: 'Website',
description: 'URL to the marketing site / channel for the project',
placeholder: 'e.g. start9.com',
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
required: false,
masked: false,
default: basicInfo?.['marketing-site'] || '',

View File

@@ -342,28 +342,35 @@ function getWifiValueSpec(
warning: null,
spec: {
ssid: {
type: 'string',
type: 'text',
name: 'Network SSID',
description: null,
inputmode: 'text',
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
required: true,
masked: false,
default: ssid || null,
warning: null,
},
password: {
type: 'string',
type: 'text',
name: 'Password',
description: null,
inputmode: 'text',
placeholder: null,
required: needsPW,
masked: true,
pattern: '^.{8,}$',
patternDescription: 'Must be longer than 8 characters',
minLength: null,
maxLength: null,
patterns: [
{
regex: '^.{8,}$',
description: 'Must be longer than 8 characters',
},
],
default: null,
warning: null,
},

View File

@@ -656,7 +656,7 @@ export module Mock {
version: 2,
data: {
lndconnect: {
type: 'string',
type: 'text',
inputmode: 'text',
description: 'This is some information about the thing.',
copyable: true,
@@ -670,7 +670,7 @@ export module Mock {
description: 'This is a nested thing metric',
value: {
'Last Name': {
type: 'string',
type: 'text',
inputmode: 'text',
description: 'The last name of the user',
copyable: true,
@@ -679,7 +679,7 @@ export module Mock {
value: 'Hill',
},
Age: {
type: 'string',
type: 'text',
inputmode: 'text',
description: 'The age of the user',
copyable: false,
@@ -688,7 +688,7 @@ export module Mock {
value: '35',
},
Password: {
type: 'string',
type: 'text',
inputmode: 'text',
description: 'A secret password',
copyable: true,
@@ -699,7 +699,7 @@ export module Mock {
},
},
'Another Value': {
type: 'string',
type: 'text',
inputmode: 'text',
description: 'Some more information about the service.',
copyable: false,
@@ -732,15 +732,16 @@ export module Mock {
name: 'External',
spec: {
'p2p-host': {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Public Address',
description: 'The public address of your Bitcoin Core server',
required: true,
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
minLength: 4,
maxLength: 20,
patterns: [],
warning: null,
default: null,
},
@@ -750,8 +751,10 @@ export module Mock {
description:
'The port that your Bitcoin Core P2P server is bound to',
required: true,
range: '[0,65535]',
integral: true,
min: 0,
max: 65535,
integer: true,
step: '1',
default: 8333,
placeholder: null,
warning: null,
@@ -778,36 +781,50 @@ export module Mock {
spec: {
rpcuser2: {
name: 'RPC Username',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'rpc username',
warning: null,
placeholder: null,
minLength: null,
maxLength: null,
required: true,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
patternDescription: 'must contain only letters.',
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'must contain only letters.',
},
],
masked: false,
},
rpcuser: {
name: 'RPC Username',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'rpc username',
warning: null,
placeholder: null,
required: true,
minLength: null,
maxLength: null,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
patternDescription: 'must contain only letters.',
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'must contain only letters.',
},
],
masked: false,
},
rpcpass: {
name: 'RPC User Password',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'rpc password',
placeholder: null,
minLength: null,
maxLength: null,
warning: null,
required: true,
default: {
@@ -815,24 +832,24 @@ export module Mock {
len: 20,
},
masked: true,
pattern: null,
patternDescription: null,
patterns: [],
},
rpcpass2: {
name: 'RPC User Password',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'rpc password',
warning: null,
placeholder: null,
minLength: null,
maxLength: null,
required: true,
default: {
charset: 'a-z,A-Z,2-9',
len: 20,
},
masked: true,
pattern: null,
patternDescription: null,
patterns: [],
},
},
},
@@ -843,12 +860,14 @@ export module Mock {
type: 'textarea',
description: 'Your personal bio',
placeholder: 'Tell the world about yourself',
minLength: null,
maxLength: null,
warning: null,
required: false,
},
testnet: {
name: 'Testnet',
type: 'boolean',
type: 'toggle',
description:
'<ul><li>determines whether your node is running on testnet or mainnet</li></ul><script src="fake"></script>',
warning: 'Chain will have to resync!',
@@ -867,7 +886,8 @@ export module Mock {
type: 'list',
description: 'This is a list of objects, like users or something',
warning: null,
range: '[0,4]',
minLength: null,
maxLength: 4,
default: [
{
'first-name': 'Admin',
@@ -889,29 +909,36 @@ export module Mock {
spec: {
'first-name': {
name: 'First Name',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'User first name',
required: false,
masked: false,
minLength: 4,
maxLength: 15,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
warning: null,
default: null,
},
'last-name': {
name: 'Last Name',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'User first name',
minLength: null,
maxLength: null,
required: false,
default: {
charset: 'a-g,2-9',
len: 12,
},
pattern: '^[a-zA-Z]+$',
patternDescription: 'must contain only letters.',
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'must contain only letters.',
},
],
masked: false,
placeholder: null,
warning: null,
@@ -921,9 +948,11 @@ export module Mock {
type: 'number',
description: 'The age of the user',
required: false,
integral: false,
integer: false,
warning: 'User must be at least 18.',
range: '[18,*)',
min: 18,
max: null,
step: null,
units: null,
placeholder: null,
default: null,
@@ -949,7 +978,8 @@ export module Mock {
type: 'multiselect',
description: 'how you want to be notified',
warning: null,
range: '(1,3]',
minLength: 2,
maxLength: 3,
values: {
email: 'EEEEmail',
text: 'Texxxt',
@@ -962,14 +992,16 @@ export module Mock {
'favorite-number': {
name: 'Favorite Number',
type: 'number',
integral: false,
integer: false,
description: 'Your favorite number of all time',
placeholder: null,
warning:
'Once you set this number, it can never be changed without severe consequences.',
required: false,
default: 7,
range: '(-100,100]',
min: -99,
max: 100,
step: 'all',
units: 'BTC',
},
'unlucky-numbers': {
@@ -979,12 +1011,15 @@ export module Mock {
warning: null,
spec: {
type: 'number',
integral: false,
range: '[-100,200)',
integer: false,
min: -10,
max: 10,
step: null,
units: null,
placeholder: null,
},
range: '[0,10]',
minLength: null,
maxLength: 10,
default: [2, 3],
},
rpcsettings: {
@@ -1001,27 +1036,29 @@ export module Mock {
spec: {
law1: {
name: 'First Law',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the first law',
required: false,
masked: false,
minLength: null,
maxLength: null,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
warning: null,
default: null,
},
law2: {
name: 'Second Law',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the second law',
required: false,
masked: false,
minLength: null,
maxLength: null,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
warning: null,
default: null,
},
@@ -1032,7 +1069,8 @@ export module Mock {
type: 'list',
description: 'the people who make the rules',
warning: null,
range: '[0,2]',
minLength: 1,
maxLength: 3,
default: [],
spec: {
type: 'object',
@@ -1041,30 +1079,37 @@ export module Mock {
spec: {
rulemakername: {
name: 'Rulemaker Name',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the name of the rule maker',
required: true,
minLength: null,
maxLength: 30,
default: {
charset: 'a-g,2-9',
len: 12,
},
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
warning: null,
},
rulemakerip: {
name: 'Rulemaker IP',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the ip of the rule maker',
required: true,
default: '192.168.1.0',
pattern:
'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
patternDescription: 'may only contain numbers and periods',
minLength: 4,
maxLength: 20,
patterns: [
{
regex:
'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
description: 'may only contain numbers and periods',
},
],
masked: false,
placeholder: null,
warning: null,
@@ -1074,22 +1119,30 @@ export module Mock {
},
rpcuser: {
name: 'RPC Username',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'rpc username',
required: true,
minLength: null,
maxLength: null,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
patternDescription: 'must contain only letters.',
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'must contain only letters.',
},
],
masked: false,
placeholder: null,
warning: null,
},
rpcpass: {
name: 'RPC User Password',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'rpc password',
minLength: null,
maxLength: null,
required: true,
default: {
charset: 'a-z,A-Z,2-9',
@@ -1097,8 +1150,7 @@ export module Mock {
},
masked: true,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
warning: null,
},
},
@@ -1115,14 +1167,20 @@ export module Mock {
name: 'Dummy',
spec: {
name: {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Name',
description: null,
minLength: null,
maxLength: null,
required: true,
masked: false,
pattern: '^[a-zA-Z]+$',
patternDescription: 'Must contain only letters.',
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'must contain only letters.',
},
],
placeholder: null,
warning: null,
default: null,
@@ -1140,28 +1198,35 @@ export module Mock {
warning: null,
spec: {
name: {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Name',
description: null,
required: true,
minLength: null,
maxLength: null,
masked: false,
pattern: '^[a-zA-Z]+$',
patternDescription: 'Must contain only letters.',
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'must contain only letters.',
},
],
placeholder: null,
warning: null,
default: null,
},
email: {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Email',
description: null,
required: true,
minLength: null,
maxLength: null,
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
warning: null,
default: null,
},
@@ -1169,27 +1234,29 @@ export module Mock {
},
'public-domain': {
name: 'Public Domain',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the public address of the node',
required: true,
default: 'bitcoinnode.com',
pattern: '.*',
patternDescription: 'anything',
minLength: null,
maxLength: null,
patterns: [],
masked: false,
placeholder: null,
warning: null,
},
'private-domain': {
name: 'Private Domain',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the private address of the node',
required: true,
minLength: null,
maxLength: null,
masked: true,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
warning: null,
default: null,
},
@@ -1200,27 +1267,30 @@ export module Mock {
port: {
name: 'Port',
type: 'number',
integral: true,
integer: true,
description:
'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444',
warning: null,
required: true,
range: '(0, 9998]',
min: 1,
max: 9999,
step: '1',
units: null,
placeholder: null,
default: null,
},
'favorite-slogan': {
name: 'Favorite Slogan',
type: 'string',
type: 'text',
inputmode: 'text',
description:
'You most favorite slogan in the whole world, used for paying you.',
required: false,
masked: true,
minLength: null,
maxLength: null,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
warning: null,
default: null,
},
@@ -1231,16 +1301,23 @@ export module Mock {
'external ip addresses that are authorized to access your Bitcoin node',
warning:
'Any IP you allow here will have RPC access to your Bitcoin node.',
range: '[1,10]',
minLength: 1,
maxLength: 10,
default: ['192.168.1.1'],
spec: {
type: 'string',
type: 'text',
inputmode: 'text',
masked: false,
placeholder: null,
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]$))',
patternDescription: 'must be a valid ipv4, ipv6, or domain name',
minLength: 4,
maxLength: 20,
patterns: [
{
regex:
'((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]$))',
description: 'must be a valid ipv4, ipv6, or domain name',
},
],
},
},
rpcauth: {
@@ -1248,15 +1325,17 @@ export module Mock {
type: 'list',
description: 'api keys that are authorized to access your Bitcoin node.',
warning: null,
range: '[0,*)',
minLength: null,
maxLength: null,
default: [],
spec: {
type: 'string',
type: 'text',
inputmode: 'text',
masked: false,
minLength: null,
maxLength: null,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
},
},
'more-advanced': {
@@ -1280,40 +1359,43 @@ export module Mock {
spec: {
law1: {
name: 'First Law',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the first law',
required: false,
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
warning: null,
default: null,
},
law2: {
name: 'Second Law',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the second law',
required: false,
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
warning: null,
default: null,
},
law4: {
name: 'Fourth Law',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the fourth law',
required: false,
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
warning: null,
default: null,
},
@@ -1322,7 +1404,8 @@ export module Mock {
type: 'list',
description: 'the third law',
warning: null,
range: '[0,2]',
minLength: null,
maxLength: 2,
default: [],
spec: {
type: 'object',
@@ -1331,7 +1414,7 @@ export module Mock {
spec: {
lawname: {
name: 'Law Name',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the name of the law maker',
required: true,
@@ -1341,21 +1424,27 @@ export module Mock {
},
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
warning: null,
},
lawagency: {
name: 'Law agency',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the ip of the law maker',
required: true,
default: '192.168.1.0',
pattern:
'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
patternDescription:
'may only contain numbers and periods',
minLength: null,
maxLength: null,
patterns: [
{
regex:
'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
description: 'may only contain numbers and periods',
},
],
masked: false,
placeholder: null,
warning: null,
@@ -1365,14 +1454,15 @@ export module Mock {
},
law5: {
name: 'Fifth Law',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the fifth law',
required: false,
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
minLength: null,
maxLength: null,
patterns: [],
warning: null,
default: null,
},
@@ -1383,7 +1473,8 @@ export module Mock {
type: 'list',
description: 'the people who make the rules',
warning: null,
range: '[0,2]',
minLength: null,
maxLength: 2,
default: [],
spec: {
type: 'object',
@@ -1392,7 +1483,7 @@ export module Mock {
spec: {
rulemakername: {
name: 'Rulemaker Name',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the name of the rule maker',
required: true,
@@ -1402,20 +1493,27 @@ export module Mock {
},
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
warning: null,
},
rulemakerip: {
name: 'Rulemaker IP',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'the ip of the rule maker',
required: true,
default: '192.168.1.0',
pattern:
'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
patternDescription: 'may only contain numbers and periods',
minLength: null,
maxLength: null,
patterns: [
{
regex:
'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$',
description: 'may only contain numbers and periods',
},
],
masked: false,
placeholder: null,
warning: null,
@@ -1425,20 +1523,26 @@ export module Mock {
},
rpcuser: {
name: 'RPC Username',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'rpc username',
required: true,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
patternDescription: 'must contain only letters.',
minLength: null,
maxLength: null,
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'must contain only letters.',
},
],
masked: false,
placeholder: null,
warning: null,
},
rpcpass: {
name: 'RPC User Password',
type: 'string',
type: 'text',
inputmode: 'text',
description: 'rpc password',
required: true,
@@ -1448,8 +1552,9 @@ export module Mock {
},
masked: true,
placeholder: null,
pattern: null,
patternDescription: null,
patterns: [],
minLength: null,
maxLength: null,
warning: null,
},
},
@@ -1550,15 +1655,21 @@ export module Mock {
group: null,
'input-spec': {
reason: {
type: 'string',
type: 'text',
inputmode: 'text',
name: 'Re-sync Reason',
description: 'Your reason for re-syncing. Why are you doing this?',
placeholder: null,
required: true,
masked: false,
pattern: '^[a-zA-Z]+$',
patternDescription: 'Must contain only letters.',
minLength: null,
maxLength: null,
patterns: [
{
regex: '^[a-zA-Z]+$',
description: 'must contain only letters.',
},
],
warning: null,
default: null,
},

View File

@@ -7,14 +7,14 @@ import {
ValidatorFn,
Validators,
} from '@angular/forms'
import { getDefaultString, Range } from '../util/config-utilities'
import { getDefaultString } from '../util/config-utilities'
import {
InputSpec,
isValueSpecListOf,
ListValueSpecNumber,
ListValueSpecObject,
ListValueSpecOf,
ListValueSpecString,
ListValueSpecText,
UniqueBy,
ValueSpec,
ValueSpecSelect,
@@ -23,7 +23,7 @@ import {
ValueSpecList,
ValueSpecNumber,
ValueSpecObject,
ValueSpecString,
ValueSpecText,
ValueSpecUnion,
unionSelectKey,
ValueSpecTextarea,
@@ -76,7 +76,7 @@ export class FormService {
getListItem(spec: ValueSpecList, entry?: any) {
const listItemValidators = getListItemValidators(spec)
if (isValueSpecListOf(spec, 'string')) {
if (isValueSpecListOf(spec, 'text')) {
return this.formBuilder.control(entry, listItemValidators)
} else if (isValueSpecListOf(spec, 'number')) {
return this.formBuilder.control(entry, listItemValidators)
@@ -106,7 +106,7 @@ export class FormService {
): UntypedFormGroup | UntypedFormArray | UntypedFormControl {
let value: any
switch (spec.type) {
case 'string':
case 'text':
if (currentValue !== undefined) {
value = currentValue
} else {
@@ -145,7 +145,7 @@ export class FormService {
spec,
isValid ? currentSelection : spec.default,
)
case 'boolean':
case 'toggle':
value = currentValue === undefined ? spec.default : currentValue
return this.formBuilder.control(value)
case 'select':
@@ -161,7 +161,7 @@ export class FormService {
}
function getListItemValidators(spec: ValueSpecList) {
if (isValueSpecListOf(spec, 'string')) {
if (isValueSpecListOf(spec, 'text')) {
return stringValidators(spec.spec)
} else if (isValueSpecListOf(spec, 'number')) {
return numberValidators(spec.spec)
@@ -169,16 +169,18 @@ function getListItemValidators(spec: ValueSpecList) {
}
function stringValidators(
spec: ValueSpecString | ListValueSpecString,
spec: ValueSpecText | ListValueSpecText,
): ValidatorFn[] {
const validators: ValidatorFn[] = []
if ((spec as ValueSpecString).required) {
if ((spec as ValueSpecText).required) {
validators.push(Validators.required)
}
if (spec.pattern) {
validators.push(Validators.pattern(spec.pattern))
validators.push(textLengthInRange(spec.minLength, spec.maxLength))
if (spec.patterns.length) {
spec.patterns.forEach(p => validators.push(Validators.pattern(p.regex)))
}
return validators
@@ -205,11 +207,11 @@ function numberValidators(
validators.push(Validators.required)
}
if (spec.integral) {
if (spec.integer) {
validators.push(isInteger())
}
validators.push(numberInRange(spec.range))
validators.push(numberInRange(spec.min, spec.max))
return validators
}
@@ -226,13 +228,13 @@ function selectValidators(spec: ValueSpecSelect): ValidatorFn[] {
function multiselectValidators(spec: ValueSpecMultiselect): ValidatorFn[] {
const validators: ValidatorFn[] = []
validators.push(listInRange(spec.range))
validators.push(listInRange(spec.minLength, spec.maxLength))
return validators
}
function listValidators(spec: ValueSpecList): ValidatorFn[] {
const validators: ValidatorFn[] = []
validators.push(listInRange(spec.range))
validators.push(listInRange(spec.minLength, spec.maxLength))
validators.push(listItemIssue())
return validators
}
@@ -247,16 +249,20 @@ function fileValidators(spec: ValueSpecFile): ValidatorFn[] {
return validators
}
export function numberInRange(stringRange: string = ''): ValidatorFn {
export function numberInRange(
min: number | null,
max: number | null,
): ValidatorFn {
return control => {
const value = control.value
if (!value) return null
try {
Range.from(stringRange).checkIncludes(value)
return null
} catch (e: any) {
return { numberNotInRange: `Number must be ${e.message}` }
}
if (typeof value !== 'number') return null
if (min && value < min)
return {
numberNotInRange: `Number must be greater than or equal to ${min}`,
}
if (max && value > max)
return { numberNotInRange: `Number must be less than or equal to ${max}` }
return null
}
}
@@ -272,14 +278,38 @@ export function isInteger(): ValidatorFn {
: { numberNotInteger: 'Must be an integer' }
}
export function listInRange(stringRange: string = ''): ValidatorFn {
export function listInRange(
minLength: number | null,
maxLength: number | null,
): ValidatorFn {
return control => {
try {
Range.from(stringRange).checkIncludes(control.value.length)
return null
} catch (e: any) {
return { listNotInRange: `List must be ${e.message}` }
}
const length = control.value.length
if (minLength && length < minLength)
return {
listNotInRange: `List must contain at least ${minLength} entries`,
}
if (maxLength && length > maxLength)
return {
listNotInRange: `List cannot contain more than ${maxLength} entries`,
}
return null
}
}
export function textLengthInRange(
minLength: number | null,
maxLength: number | null,
): ValidatorFn {
return control => {
const value = control.value
if (value === null || value === undefined) return null
const length = value.length
if (minLength && length < minLength)
return { listNotInRange: `Must be at least ${minLength} characters` }
if (maxLength && length > maxLength)
return { listNotInRange: `Cannot be great than ${maxLength} characters` }
return null
}
}
@@ -335,7 +365,7 @@ export function listUnique(spec: ValueSpecList): ValidatorFn {
function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean {
// TODO: fix types
switch (spec.spec.type) {
case 'string':
case 'text':
case 'number':
return val1 == val2
case 'object':
@@ -348,10 +378,10 @@ function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean {
function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean {
switch (spec.type) {
case 'string':
case 'text':
case 'textarea':
case 'number':
case 'boolean':
case 'toggle':
case 'select':
return val1 == val2
case 'object':
@@ -536,7 +566,7 @@ export function convertValuesRecursive(
control.setValue(
control.value || control.value === 0 ? Number(control.value) : null,
)
} else if (valueSpec.type === 'string' || valueSpec.type === 'textarea') {
} else if (valueSpec.type === 'text' || valueSpec.type === 'textarea') {
if (!control.value) control.setValue(null)
} else if (valueSpec.type === 'object') {
convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup)
@@ -553,7 +583,7 @@ export function convertValuesRecursive(
controls.forEach(control => {
control.setValue(control.value ? Number(control.value) : null)
})
} else if (valueSpec.spec.type === 'string') {
} else if (valueSpec.spec.type === 'text') {
controls.forEach(control => {
if (!control.value) control.setValue(null)
})