diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2496e0fcf..9b7a5b04f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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" diff --git a/frontend/package.json b/frontend/package.json index 90474eab2..16b8e36d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts index a63b81520..6d9c1fd1c 100644 --- a/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts +++ b/frontend/projects/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -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, diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.html b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.html index bb8914c62..3112793b5 100644 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.html +++ b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.html @@ -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 @@ > + @Input() spec!: ValueSpecOf<'text' | 'textarea' | 'number'> @Input() control!: FormControl @Output() onInputChange = new EventEmitter() diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.html b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.html index 3c8febdef..ba236e1f1 100644 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.html +++ b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.html @@ -8,7 +8,7 @@ > + @Input() spec!: ValueSpecOf<'toggle' | 'select' | 'multiselect'> @Input() control!: FormControl @Input() name!: string diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.html b/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.html index 6b3627b36..40f534ddc 100644 --- a/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.html +++ b/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.html @@ -10,7 +10,7 @@
- + - + diff --git a/frontend/projects/ui/src/app/components/form/form-control/form-control.component.ts b/frontend/projects/ui/src/app/components/form/form-control/form-control.component.ts index ffc477184..43e4f3dff 100644 --- a/frontend/projects/ui/src/app/components/form/form-control/form-control.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-control/form-control.component.ts @@ -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) => ({ + useFactory: (control: FormControlComponent) => ({ required: 'Required', - pattern: () => control.spec.patternDescription, + patterns: () => control.spec.patterns.map(p => p.regex), // @TODO Alex }), }, ], diff --git a/frontend/projects/ui/src/app/components/form/form-number/form-number.component.html b/frontend/projects/ui/src/app/components/form/form-number/form-number.component.html index a681530c3..ef90fc62e 100644 --- a/frontend/projects/ui/src/app/components/form/form-number/form-number.component.html +++ b/frontend/projects/ui/src/app/components/form/form-number/form-number.component.html @@ -1,10 +1,12 @@ + diff --git a/frontend/projects/ui/src/app/components/form/form-number/form-number.component.ts b/frontend/projects/ui/src/app/components/form/form-number/form-number.component.ts index 2d2963f48..29d53e018 100644 --- a/frontend/projects/ui/src/app/components/form/form-number/form-number.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-number/form-number.component.ts @@ -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 { 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 } } diff --git a/frontend/projects/ui/src/app/components/form/form-string/form-string.component.html b/frontend/projects/ui/src/app/components/form/form-string/form-string.component.html deleted file mode 100644 index 1561861a1..000000000 --- a/frontend/projects/ui/src/app/components/form/form-string/form-string.component.html +++ /dev/null @@ -1,27 +0,0 @@ - - {{ spec.name }} - * - - - - - diff --git a/frontend/projects/ui/src/app/components/form/form-string/form-string.component.ts b/frontend/projects/ui/src/app/components/form/form-string/form-string.component.ts deleted file mode 100644 index b59065701..000000000 --- a/frontend/projects/ui/src/app/components/form/form-string/form-string.component.ts +++ /dev/null @@ -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 { - masked = true -} diff --git a/frontend/projects/ui/src/app/components/form/form-text/form-text.component.html b/frontend/projects/ui/src/app/components/form/form-text/form-text.component.html index ddaca759e..9dbcd02f1 100644 --- a/frontend/projects/ui/src/app/components/form/form-text/form-text.component.html +++ b/frontend/projects/ui/src/app/components/form/form-text/form-text.component.html @@ -1,11 +1,29 @@ - {{ spec.name }} * - - + + + + + diff --git a/frontend/projects/ui/src/app/components/form/form-string/form-string.component.scss b/frontend/projects/ui/src/app/components/form/form-text/form-text.component.scss similarity index 100% rename from frontend/projects/ui/src/app/components/form/form-string/form-string.component.scss rename to frontend/projects/ui/src/app/components/form/form-text/form-text.component.scss diff --git a/frontend/projects/ui/src/app/components/form/form-text/form-text.component.ts b/frontend/projects/ui/src/app/components/form/form-text/form-text.component.ts index 3b42764b6..0baa92411 100644 --- a/frontend/projects/ui/src/app/components/form/form-text/form-text.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-text/form-text.component.ts @@ -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 {} +export class FormTextComponent extends Control { + masked = true +} diff --git a/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html b/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html new file mode 100644 index 000000000..ddaca759e --- /dev/null +++ b/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html @@ -0,0 +1,11 @@ + + {{ spec.name }} + * + + diff --git a/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts b/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts new file mode 100644 index 000000000..47a6ad315 --- /dev/null +++ b/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/components/form/form-boolean/form-boolean.component.html b/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html similarity index 100% rename from frontend/projects/ui/src/app/components/form/form-boolean/form-boolean.component.html rename to frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html diff --git a/frontend/projects/ui/src/app/components/form/form-boolean/form-boolean.component.scss b/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.scss similarity index 100% rename from frontend/projects/ui/src/app/components/form/form-boolean/form-boolean.component.scss rename to frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.scss diff --git a/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts b/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts new file mode 100644 index 000000000..debbe6a67 --- /dev/null +++ b/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts @@ -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 {} diff --git a/frontend/projects/ui/src/app/components/form/form.module.ts b/frontend/projects/ui/src/app/components/form/form.module.ts index 92c1ebbdf..842661b5d 100644 --- a/frontend/projects/ui/src/app/components/form/form.module.ts +++ b/frontend/projects/ui/src/app/components/form/form.module.ts @@ -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, diff --git a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts index 7e73db06e..997f50b84 100644 --- a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts +++ b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts @@ -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, diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts index daecd00a7..f5c5bcbb6 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts @@ -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'], }, } diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts index 4efadf47d..95bd29b1a 100644 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts +++ b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts @@ -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'] || '', diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index 41038a5e7..d5821c71e 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -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, }, diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index 4539aa542..e4aebc488 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -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: '
  • determines whether your node is running on testnet or mainnet
', 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, }, diff --git a/frontend/projects/ui/src/app/services/form.service.ts b/frontend/projects/ui/src/app/services/form.service.ts index ac58f431c..75ee8bae6 100644 --- a/frontend/projects/ui/src/app/services/form.service.ts +++ b/frontend/projects/ui/src/app/services/form.service.ts @@ -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) })