update union types and camel case for specs

This commit is contained in:
Matt Hill
2023-03-28 09:52:29 -06:00
committed by Aiden McClelland
parent 4a6a3da36c
commit c7438c4aff
25 changed files with 252 additions and 380 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.alpha9",
"start-sdk": "^0.4.0-lib0.beta3",
"swiper": "^8.2.4",
"ts-matches": "^5.2.1",
"tslib": "^2.3.0",
@@ -13771,9 +13771,9 @@
}
},
"node_modules/start-sdk": {
"version": "0.4.0-lib0.alpha9",
"resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.alpha9.tgz",
"integrity": "sha512-F+ekxjVEKgNv7SU5XCe1rn7PqbKsbqCSS7pecMxcKQmbNlic4iyo3+lIS9JuPBSyTTjJlzJFzkdaxGwf4h83mg==",
"version": "0.4.0-lib0.beta3",
"resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.beta3.tgz",
"integrity": "sha512-MuKc4QB6rR2UOVZaT3MApJf5jGtIzUmKGLS4mm6Zqvmud+MP/FQTRB8y7CZjyBmBGXQZcSyFUcEBkR/hQTmLXA==",
"dependencies": {
"@iarna/toml": "^2.2.5",
"lodash": "^4.17.21",

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.alpha9",
"start-sdk": "^0.4.0-lib0.beta3",
"swiper": "^8.2.4",
"ts-matches": "^5.2.1",
"tslib": "^2.3.0",

View File

@@ -12,7 +12,7 @@ import {
ModalController,
} from '@ionic/angular'
import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page'
import { InputSpec } from 'start-sdk/types/config-types'
import { InputSpec } from 'start-sdk/lib/config/config-types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ErrorToastService } from '@start9labs/shared'
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
@@ -282,7 +282,7 @@ const CifsSpec: InputSpec = {
'The hostname of your target device on the Local Area Network.',
placeholder: `e.g. 'My Computer' OR 'my-computer.local'`,
pattern: '^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$',
'pattern-description': `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
patternDescription: `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`,
nullable: false,
masked: false,
default: null,
@@ -295,7 +295,7 @@ const CifsSpec: InputSpec = {
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).`,
placeholder: 'e.g. my-shared-folder or /Desktop/my-folder',
pattern: null,
'pattern-description': null,
patternDescription: null,
nullable: false,
masked: false,
default: null,
@@ -308,7 +308,7 @@ const CifsSpec: InputSpec = {
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.`,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
nullable: false,
masked: false,
default: null,
@@ -321,7 +321,7 @@ const CifsSpec: InputSpec = {
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.`,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
nullable: true,
masked: true,
default: null,

View File

@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core'
import { AbstractControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
import { ValueSpecOf } from 'start-sdk/lib/config/config-types'
@Component({
selector: 'form-file',

View File

@@ -57,6 +57,6 @@
</ion-item>
<p class="error-message">
<span *ngIf="control.errors as errors">
{{ errors | getError : $any(spec)['pattern-description'] }}
{{ errors | getError : $any(spec).patternDescription }}
</span>
</p>

View File

@@ -1,6 +1,6 @@
import { Component, Input, inject, Output, EventEmitter } from '@angular/core'
import { FormControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
import { ValueSpecOf } from 'start-sdk/lib/config/config-types'
import { THEME } from '@start9labs/shared'
@Component({

View File

@@ -31,11 +31,14 @@
[selectedText]="
spec.type === 'multiselect' && control.value?.length > 1
? '[' + control.value.length + ' selected]'
: spec['value-names'][control.value]
: spec.values[control.value]
"
>
<ion-select-option *ngFor="let option of spec.values" [value]="option">
{{ spec['value-names'][option] }}
<ion-select-option
*ngFor="let option of spec.values | keyvalue"
[value]="option.key"
>
{{ option.value }}
</ion-select-option>
</ion-select>
</ion-item>

View File

@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core'
import { FormControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
import { ValueSpecOf } from 'start-sdk/lib/config/config-types'
@Component({
selector: 'form-select',

View File

@@ -1,6 +1,6 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'
import { AbstractControl } from '@angular/forms'
import { ValueSpecOf } from 'start-sdk/types/config-types'
import { ValueSpecOf } from 'start-sdk/lib/config/config-types'
@Component({
selector: 'form-subform',

View File

@@ -90,11 +90,9 @@
let i = index
"
>
<!-- object or union -->
<ng-container
*ngIf="spec.subtype === 'object' || spec.subtype === 'union'"
>
<!-- object/union label -->
<!-- object -->
<ng-container *ngIf="spec.subtype === 'object'">
<!-- object label -->
<ion-item
button
(click)="toggleExpandListObject(entry.key, i)"
@@ -137,16 +135,6 @@
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
"
></form-object>
<form-union
*ngIf="spec.subtype === 'union'"
[spec]="$any(spec.spec)"
[formGroup]="abstractControl"
[current]="current?.[entry.key]?.[i]"
[original]="original?.[entry.key]?.[i]"
(onInputChange)="
updateLabel(entry.key, i, $any(spec.spec)['display-as'])
"
></form-union>
<div style="text-align: right; padding-top: 12px">
<ion-button
fill="clear"
@@ -191,7 +179,7 @@
$any(formGroup.get(entry.key))?.at(i)?.errors as errors
"
>
{{ errors | getError : $any(spec)['pattern-description'] }}
{{ errors | getError : $any(spec).patternDescription }}
</span>
</p>
</div>

View File

@@ -8,7 +8,7 @@ import {
SimpleChanges,
} from '@angular/core'
import { UntypedFormArray, UntypedFormGroup } from '@angular/forms'
import { AlertButton, AlertController, ModalController } from '@ionic/angular'
import { AlertButton, AlertController } from '@ionic/angular'
import {
InputSpec,
ListValueSpecOf,
@@ -16,7 +16,7 @@ import {
ValueSpecBoolean,
ValueSpecList,
ValueSpecUnion,
} from 'start-sdk/types/config-types'
} from 'start-sdk/lib/config/config-types'
import { FormService } from 'src/app/services/form.service'
import { THEME, pauseFor } from '@start9labs/shared'
import { v4 } from 'uuid'
@@ -49,7 +49,6 @@ export class FormObjectComponent {
constructor(
private readonly alertCtrl: AlertController,
private readonly modalCtrl: ModalController,
private readonly formService: FormService,
@Inject(DOCUMENT) private readonly document: Document,
) {}
@@ -86,9 +85,7 @@ export class FormObjectComponent {
if (spec.type === 'list' && ['object', 'union'].includes(spec.subtype)) {
this.objectListDisplay[key] = []
this.formGroup.get(key)?.value.forEach((obj: any, index: number) => {
const displayAs = (spec.spec as ListValueSpecOf<'object'>)[
'display-as'
]
const displayAs = (spec.spec as ListValueSpecOf<'object'>).displayAs
this.objectListDisplay[key][index] = {
expanded: false,
displayAs: displayAs
@@ -213,9 +210,7 @@ export class FormObjectComponent {
arr.insert(index, newItem)
if (['object', 'union'].includes(listSpec.subtype)) {
const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)[
'display-as'
]
const displayAs = (listSpec.spec as ListValueSpecOf<'object'>).displayAs
this.objectListDisplay[key].push({
expanded: false,
displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '',

View File

@@ -3,8 +3,8 @@
<ion-item-divider [class.error-border]="formGroup.invalid">
<form-label
[data]="{
name: spec.tag.name,
description: spec.tag.description,
name: spec.name,
description: spec.description,
newOptions: hasNewOptions,
edited: formGroup.dirty
}"
@@ -12,13 +12,13 @@
<!-- class enter-click disables the enter click on the modal behind the select -->
<ion-select
[interfaceOptions]="{
header: spec.tag.name,
message: spec.tag.warning | toWarningText,
header: spec.name,
message: spec.warning | toWarningText,
cssClass: 'enter-click'
}"
slot="end"
placeholder="Select"
[formControlName]="spec.tag.id"
[formControlName]="unionSelectKey"
[selectedText]="variantName"
(ionChange)="updateUnion($event)"
>
@@ -26,7 +26,7 @@
*ngFor="let option of spec.variants | keyvalue"
[value]="option.key"
>
{{ spec.tag['variant-names'][option.key] }}
{{ spec.variants[option.key].name }}
</ion-select-option>
</ion-select>
</ion-item-divider>

View File

@@ -2,7 +2,11 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { UntypedFormGroup } from '@angular/forms'
import { v4 } from 'uuid'
import { FormService } from 'src/app/services/form.service'
import { ValueSpecUnion, InputSpec } from 'start-sdk/types/config-types'
import {
ValueSpecUnion,
InputSpec,
unionSelectKey,
} from 'start-sdk/lib/config/config-types'
@Component({
selector: 'form-union',
@@ -11,21 +15,23 @@ import { ValueSpecUnion, InputSpec } from 'start-sdk/types/config-types'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormUnionComponent {
readonly unionSelectKey = unionSelectKey
@Input() formGroup!: UntypedFormGroup
@Input() spec!: ValueSpecUnion
@Input() current?: Record<string, any>
@Input() original?: Record<string, any>
get selectedVariant(): string {
return this.formGroup.get(this.spec.tag.id)?.value
return this.formGroup.get(unionSelectKey)?.value
}
get variantName(): string {
return this.spec.tag['variant-names'][this.selectedVariant]
return this.spec.variants[this.selectedVariant].name
}
get variantSpec(): InputSpec {
return this.spec.variants[this.selectedVariant]
return this.spec.variants[this.selectedVariant].spec
}
get hasNewOptions(): boolean {
@@ -38,10 +44,8 @@ export class FormUnionComponent {
constructor(private readonly formService: FormService) {}
updateUnion(e: any): void {
const tagId = this.spec.tag.id
Object.keys(this.formGroup.controls).forEach(control => {
if (control === tagId) return
if (control === unionSelectKey) return
this.formGroup.removeControl(control)
})
@@ -51,7 +55,7 @@ export class FormUnionComponent {
)
Object.keys(unionGroup.controls).forEach(control => {
if (control === tagId) return
if (control === unionSelectKey) return
this.formGroup.addControl(control, unionGroup.controls[control])
})
}

View File

@@ -1,5 +1,5 @@
import { Directive } from '@angular/core'
import { ValueSpec, ValueSpecUnion } from 'start-sdk/types/config-types'
import { ValueSpec, ValueSpecUnion } from 'start-sdk/lib/config/config-types'
import { AlertButton, AlertController } from '@ionic/angular'
@Directive({

View File

@@ -13,7 +13,7 @@ import {
isObject,
} from '@start9labs/shared'
import { DependentInfo } from 'src/app/types/dependent-info'
import { InputSpec } from 'start-sdk/types/config-types'
import { InputSpec } from 'start-sdk/lib/config/config-types'
import {
DataModel,
PackageDataEntry,

View File

@@ -5,7 +5,7 @@ import {
convertValuesRecursive,
FormService,
} from 'src/app/services/form.service'
import { InputSpec } from 'start-sdk/types/config-types'
import { InputSpec } from 'start-sdk/lib/config/config-types'
export interface ActionButton {
text: string

View File

@@ -14,7 +14,7 @@ import { ActionSheetButton } from '@ionic/core'
import { ErrorToastService, sameUrl, toUrl } from '@start9labs/shared'
import { AbstractMarketplaceService } from '@start9labs/marketplace'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ValueSpecObject } from 'start-sdk/types/config-types'
import { ValueSpecObject } from 'start-sdk/lib/config/config-types'
import {
GenericFormPage,
GenericFormOptions,
@@ -276,7 +276,7 @@ function getMarketplaceValueSpec(): ValueSpecObject {
nullable: false,
masked: false,
pattern: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`,
'pattern-description': 'Must be a valid URL',
patternDescription: 'Must be a valid URL',
placeholder: 'e.g. https://example.org',
default: null,
textarea: false,

View File

@@ -12,7 +12,7 @@ import {
} from 'src/app/modals/generic-input/generic-input.component'
import { PatchDB } from 'patch-db-client'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { InputSpec } from 'start-sdk/types/config-types'
import { InputSpec } from 'start-sdk/lib/config/config-types'
import * as yaml from 'js-yaml'
import { v4 } from 'uuid'
import { DataModel, DevData } from 'src/app/services/patch-db/data-model'
@@ -227,7 +227,7 @@ const SAMPLE_CONFIG: InputSpec = {
description: 'Example description for required string input.',
placeholder: 'Enter string value',
pattern: '^[a-zA-Z0-9! _]+$',
'pattern-description': 'Must be alphanumeric (may contain underscore).',
patternDescription: 'Must be alphanumeric (may contain underscore).',
default: null,
textarea: false,
warning: null,
@@ -256,8 +256,7 @@ const SAMPLE_CONFIG: InputSpec = {
'sample-select': {
type: 'multiselect',
name: 'Example Enum Select',
values: ['red', 'blue', 'green'],
'value-names': {
values: {
red: 'Red',
blue: 'Blue',
green: 'Green',

View File

@@ -1,4 +1,4 @@
import { InputSpec } from 'start-sdk/types/config-types'
import { InputSpec } from 'start-sdk/lib/config/config-types'
import { DevProjectData } from 'src/app/services/patch-db/data-model'
export type BasicInfo = {
@@ -28,7 +28,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
nullable: false,
masked: false,
pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$',
'pattern-description': 'Must be kebab case',
patternDescription: 'Must be kebab case',
default: basicInfo?.id || '',
textarea: false,
warning: null,
@@ -41,7 +41,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
nullable: false,
masked: false,
pattern: null,
'pattern-description': null,
patternDescription: null,
default: basicInfo ? basicInfo.title : devData.name,
textarea: false,
warning: null,
@@ -55,7 +55,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
nullable: false,
masked: false,
pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$',
'pattern-description': 'Must be valid Emver version',
patternDescription: 'Must be valid Emver version',
default: basicInfo?.['service-version-number'] || '',
textarea: false,
warning: null,
@@ -77,7 +77,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
textarea: true,
default: basicInfo?.description?.short || '',
pattern: '^.{1,320}$',
'pattern-description': 'Must be shorter than 320 characters',
patternDescription: 'Must be shorter than 320 characters',
warning: null,
},
long: {
@@ -90,7 +90,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
textarea: true,
default: basicInfo?.description?.long || '',
pattern: '^.{1,5000}$',
'pattern-description': 'Must be shorter than 5000 characters',
patternDescription: 'Must be shorter than 5000 characters',
warning: null,
},
},
@@ -104,7 +104,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
nullable: false,
masked: false,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: true,
default: basicInfo?.['release-notes'] || '',
warning: null,
@@ -113,18 +113,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
type: 'select',
name: 'License',
warning: null,
values: [
'gnu-agpl-v3',
'gnu-gpl-v3',
'gnu-lgpl-v3',
'mozilla-public-license-2.0',
'apache-license-2.0',
'mit',
'boost-software-license-1.0',
'the-unlicense',
'custom',
],
'value-names': {
values: {
'gnu-agpl-v3': 'GNU AGPLv3',
'gnu-gpl-v3': 'GNU GPLv3',
'gnu-lgpl-v3': 'GNU LGPLv3',
@@ -136,6 +125,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
custom: 'Custom',
},
description: 'Example description for select',
nullable: false,
default: 'mit',
},
'wrapper-repo': {
@@ -145,7 +135,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
'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,
'pattern-description': null,
patternDescription: null,
nullable: false,
masked: false,
default: basicInfo?.['wrapper-repo'] || '',
@@ -158,7 +148,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
description: 'The original project repository URL',
placeholder: 'e.g. www.github.com/example',
pattern: null,
'pattern-description': null,
patternDescription: null,
nullable: true,
masked: false,
default: basicInfo?.['upstream-repo'] || '',
@@ -171,7 +161,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
description: 'URL to the support site / channel for the project',
placeholder: 'e.g. start9.com/support',
pattern: null,
'pattern-description': null,
patternDescription: null,
nullable: true,
masked: false,
default: basicInfo?.['support-site'] || '',
@@ -184,7 +174,7 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec {
description: 'URL to the marketing site / channel for the project',
placeholder: 'e.g. start9.com',
pattern: null,
'pattern-description': null,
patternDescription: null,
nullable: true,
masked: false,
default: basicInfo?.['marketing-site'] || '',

View File

@@ -9,7 +9,7 @@ import {
import { AlertInput } from '@ionic/core'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ActionSheetButton } from '@ionic/core'
import { ValueSpecObject } from 'start-sdk/types/config-types'
import { ValueSpecObject } from 'start-sdk/lib/config/config-types'
import { RR } from 'src/app/services/api/api.types'
import { pauseFor, ErrorToastService } from '@start9labs/shared'
import {
@@ -361,7 +361,7 @@ function getWifiValueSpec(
description: null,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
nullable: false,
masked: false,
default: ssid || null,
@@ -376,7 +376,7 @@ function getWifiValueSpec(
nullable: !needsPW,
masked: true,
pattern: '^.{8,}$',
'pattern-description': 'Must be longer than 8 characters',
patternDescription: 'Must be longer than 8 characters',
default: null,
textarea: false,
warning: null,

View File

@@ -714,46 +714,43 @@ export module Mock {
spec: {
'bitcoind-p2p': {
type: 'union',
tag: {
id: 'type',
name: 'Bitcoin Core P2P',
description:
'<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>',
'variant-names': {
internal: 'Bitcoin Core',
external: 'External Node',
},
warning: null,
},
name: 'Bitcoin Core P2P',
description:
'<p>The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:</p><ul><li><strong>Bitcoin Core</strong>: The Bitcoin Core service installed on this device</li><li><strong>External Node</strong>: A Bitcoin node running on a different device</li></ul>',
warning: null,
default: 'internal',
nullable: false,
variants: {
internal: {},
internal: { name: 'Internal', spec: {} },
external: {
'p2p-host': {
type: 'string',
name: 'Public Address',
description: 'The public address of your Bitcoin Core server',
nullable: false,
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
},
'p2p-port': {
type: 'number',
name: 'P2P Port',
description:
'The port that your Bitcoin Core P2P server is bound to',
nullable: false,
range: '[0,65535]',
integral: true,
default: 8333,
placeholder: null,
warning: null,
units: null,
name: 'External',
spec: {
'p2p-host': {
type: 'string',
name: 'Public Address',
description: 'The public address of your Bitcoin Core server',
nullable: false,
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
},
'p2p-port': {
type: 'number',
name: 'P2P Port',
description:
'The port that your Bitcoin Core P2P server is bound to',
nullable: false,
range: '[0,65535]',
integral: true,
default: 8333,
placeholder: null,
warning: null,
units: null,
},
},
},
},
@@ -782,7 +779,7 @@ export module Mock {
nullable: false,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
patternDescription: 'must contain only letters.',
masked: false,
textarea: false,
},
@@ -795,7 +792,7 @@ export module Mock {
nullable: false,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
patternDescription: 'must contain only letters.',
masked: false,
textarea: false,
},
@@ -812,7 +809,7 @@ export module Mock {
},
masked: true,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
},
rpcpass2: {
@@ -828,7 +825,7 @@ export module Mock {
},
masked: true,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
},
},
@@ -847,7 +844,6 @@ export module Mock {
name: 'Needed File',
type: 'file',
description: 'A file we need',
placeholder: null, // @TODO delete
warning: 'Testing warning',
nullable: false,
extensions: ['.png'],
@@ -873,10 +869,9 @@ export module Mock {
],
// the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements.
// it just so happens that ValueSpecObject's have the field { spec: InputSpec }
// see 'union-list' below for a different example.
spec: {
'unique-by': 'last-name',
'display-as': `I'm {{last-name}}, {{first-name}} {{last-name}}`,
uniqueBy: 'last-name',
displayAs: `I'm {{last-name}}, {{first-name}} {{last-name}}`,
spec: {
'first-name': {
name: 'First Name',
@@ -886,7 +881,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
@@ -901,7 +896,7 @@ export module Mock {
len: 12,
},
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
patternDescription: 'must contain only letters.',
masked: false,
placeholder: null,
textarea: false,
@@ -922,77 +917,10 @@ export module Mock {
},
},
},
'union-list': {
name: 'Union List',
type: 'list',
subtype: 'union',
description: 'This is a sample list of unions',
warning: 'If you change this, things may work.',
// a list of union selections. e.g. 'summer', 'winter',...
default: ['summer'],
range: '[0, 2]',
spec: {
tag: {
id: 'preference',
description: null,
warning: null,
'variant-names': {
summer: 'Summer',
winter: 'Winter',
other: 'Other',
},
name: 'Preference',
},
'display-as': null,
// this default is used to make a union selection when a new list element is first created
default: 'summer',
variants: {
summer: {
'favorite-tree': {
name: 'Favorite Tree',
type: 'string',
nullable: false,
description: 'What is your favorite tree?',
default: 'Maple',
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
},
'favorite-flower': {
name: 'Favorite Flower',
type: 'select',
description: 'Select your favorite flower',
warning: null,
'value-names': {
none: 'Hate Flowers',
red: 'Red',
blue: 'Blue',
purple: 'Purple',
},
values: ['none', 'red', 'blue', 'purple'],
default: 'none',
},
},
winter: {
'like-snow': {
name: 'Like Snow?',
type: 'boolean',
description: 'Do you like snow or not?',
warning: null,
default: true,
},
},
},
'unique-by': 'preference',
},
},
'random-select': {
name: 'Random Select',
type: 'select',
'value-names': {
values: {
hello: 'Hello',
goodbye: 'Goodbye',
sup: 'Sup',
@@ -1000,7 +928,7 @@ export module Mock {
default: 'sup',
description: 'This is not even real.',
warning: 'Be careful changing this!',
values: ['hello', 'goodbye', 'sup'],
nullable: false,
},
notifications: {
name: 'Notification Preferences',
@@ -1008,14 +936,13 @@ export module Mock {
description: 'how you want to be notified',
warning: null,
range: '(1,3]',
'value-names': {
values: {
email: 'EEEEmail',
text: 'Texxxt',
call: 'Ccccall',
push: 'PuuuusH',
webhook: 'WebHooookkeee',
},
values: ['email', 'text', 'call', 'push', 'webhook'],
default: ['email'],
},
'favorite-number': {
@@ -1066,7 +993,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
@@ -1079,7 +1006,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
@@ -1095,8 +1022,8 @@ export module Mock {
range: '[0,2]',
default: [],
spec: {
'unique-by': null,
'display-as': null,
uniqueBy: null,
displayAs: null,
spec: {
rulemakername: {
name: 'Rulemaker Name',
@@ -1110,7 +1037,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
},
@@ -1122,7 +1049,7 @@ export module Mock {
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])$',
'pattern-description': 'may only contain numbers and periods',
patternDescription: 'may only contain numbers and periods',
masked: false,
placeholder: null,
textarea: false,
@@ -1138,7 +1065,7 @@ export module Mock {
nullable: false,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
patternDescription: 'must contain only letters.',
masked: false,
placeholder: null,
textarea: false,
@@ -1156,7 +1083,7 @@ export module Mock {
masked: true,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
},
@@ -1165,78 +1092,75 @@ export module Mock {
'bitcoin-node': {
type: 'union',
default: 'internal',
tag: {
id: 'type',
'variant-names': {
internal: 'Internal',
external: 'External',
},
name: 'Bitcoin Node Settings',
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
warning: 'Careful changing this',
},
name: 'Bitcoin Node Settings',
description: 'Options<ul><li>Item 1</li><li>Item 2</li></ul>',
warning: 'Careful changing this',
nullable: false,
variants: {
internal: {},
internal: { name: 'Internal', spec: {} },
external: {
'emergency-contact': {
name: 'Emergency Contact',
type: 'object',
description: 'The person to contact in case of emergency.',
warning: null,
spec: {
name: {
type: 'string',
name: 'Name',
description: null,
nullable: false,
masked: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
placeholder: null,
textarea: false,
warning: null,
default: null,
},
email: {
type: 'string',
name: 'Email',
description: null,
nullable: false,
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
name: 'External',
spec: {
'emergency-contact': {
name: 'Emergency Contact',
type: 'object',
description: 'The person to contact in case of emergency.',
warning: null,
spec: {
name: {
type: 'string',
name: 'Name',
description: null,
nullable: false,
masked: false,
pattern: '^[a-zA-Z]+$',
patternDescription: 'Must contain only letters.',
placeholder: null,
textarea: false,
warning: null,
default: null,
},
email: {
type: 'string',
name: 'Email',
description: null,
nullable: false,
masked: false,
placeholder: null,
pattern: null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
},
},
},
},
'public-domain': {
name: 'Public Domain',
type: 'string',
description: 'the public address of the node',
nullable: false,
default: 'bitcoinnode.com',
pattern: '.*',
'pattern-description': 'anything',
masked: false,
placeholder: null,
textarea: false,
warning: null,
},
'private-domain': {
name: 'Private Domain',
type: 'string',
description: 'the private address of the node',
nullable: false,
masked: true,
placeholder: null,
pattern: null,
'pattern-description': null,
textarea: false,
warning: null,
default: null,
'public-domain': {
name: 'Public Domain',
type: 'string',
description: 'the public address of the node',
nullable: false,
default: 'bitcoinnode.com',
pattern: '.*',
patternDescription: 'anything',
masked: false,
placeholder: null,
textarea: false,
warning: null,
},
'private-domain': {
name: 'Private Domain',
type: 'string',
description: 'the private address of the node',
nullable: false,
masked: true,
placeholder: null,
pattern: null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
},
},
},
},
@@ -1263,7 +1187,7 @@ export module Mock {
masked: true,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
@@ -1283,7 +1207,7 @@ export module Mock {
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]$))',
'pattern-description': 'must be a valid ipv4, ipv6, or domain name',
patternDescription: 'must be a valid ipv4, ipv6, or domain name',
},
},
rpcauth: {
@@ -1298,7 +1222,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
},
},
'more-advanced': {
@@ -1328,7 +1252,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
@@ -1341,7 +1265,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
@@ -1354,7 +1278,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
@@ -1368,8 +1292,8 @@ export module Mock {
range: '[0,2]',
default: [],
spec: {
'unique-by': null,
'display-as': null,
uniqueBy: null,
displayAs: null,
spec: {
lawname: {
name: 'Law Name',
@@ -1383,7 +1307,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
},
@@ -1395,7 +1319,7 @@ export module Mock {
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])$',
'pattern-description':
patternDescription:
'may only contain numbers and periods',
masked: false,
placeholder: null,
@@ -1413,7 +1337,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
default: null,
@@ -1429,8 +1353,8 @@ export module Mock {
range: '[0,2]',
default: [],
spec: {
'unique-by': null,
'display-as': null,
uniqueBy: null,
displayAs: null,
spec: {
rulemakername: {
name: 'Rulemaker Name',
@@ -1444,7 +1368,7 @@ export module Mock {
masked: false,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
},
@@ -1456,8 +1380,7 @@ export module Mock {
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])$',
'pattern-description':
'may only contain numbers and periods',
patternDescription: 'may only contain numbers and periods',
masked: false,
placeholder: null,
textarea: false,
@@ -1473,7 +1396,7 @@ export module Mock {
nullable: false,
default: 'defaultrpcusername',
pattern: '^[a-zA-Z]+$',
'pattern-description': 'must contain only letters.',
patternDescription: 'must contain only letters.',
masked: false,
placeholder: null,
textarea: false,
@@ -1491,7 +1414,7 @@ export module Mock {
masked: true,
placeholder: null,
pattern: null,
'pattern-description': null,
patternDescription: null,
textarea: false,
warning: null,
},
@@ -1520,7 +1443,6 @@ export module Mock {
age: 60,
},
],
'union-list': undefined,
'random-select': ['goodbye'],
'favorite-number': 0,
rpcsettings: {
@@ -1601,7 +1523,7 @@ export module Mock {
nullable: false,
masked: false,
pattern: '^[a-zA-Z]+$',
'pattern-description': 'Must contain only letters.',
patternDescription: 'Must contain only letters.',
textarea: false,
warning: null,
default: null,

View File

@@ -1,7 +1,7 @@
import { Dump, Revision } from 'patch-db-client'
import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace'
import { PackagePropertiesVersioned } from 'src/app/util/properties.util'
import { InputSpec } from 'start-sdk/types/config-types'
import { InputSpec } from 'start-sdk/lib/config/config-types'
import {
DataModel,
DependencyError,

View File

@@ -14,7 +14,6 @@ import {
ListValueSpecObject,
ListValueSpecOf,
ListValueSpecString,
ListValueSpecUnion,
UniqueBy,
ValueSpec,
ValueSpecSelect,
@@ -25,7 +24,8 @@ import {
ValueSpecObject,
ValueSpecString,
ValueSpecUnion,
} from 'start-sdk/types/config-types'
unionSelectKey,
} from 'start-sdk/lib/config/config-types'
import { getDefaultString, Range } from '../util/config-utilities'
const Mustache = require('mustache')
@@ -37,18 +37,16 @@ export class FormService {
createForm(
spec: InputSpec,
current: { [key: string]: any } = {},
current: Record<string, any> = {},
): UntypedFormGroup {
return this.getFormGroup(spec, [], current)
}
getUnionObject(
spec: ValueSpecUnion | ListValueSpecUnion,
selection: string,
current?: { [key: string]: any } | null,
spec: ValueSpecUnion,
selection: string | null,
): UntypedFormGroup {
const { variants, tag } = spec
const { name, description, warning, 'variant-names': variantNames } = tag
const { name, description, warning, variants, nullable } = spec
const enumSpec: ValueSpecSelect = {
type: 'select',
@@ -56,14 +54,22 @@ export class FormService {
description,
warning,
default: selection,
values: Object.keys(variants),
'value-names': variantNames,
nullable,
values: Object.keys(variants).reduce(
(prev, curr) => ({
...prev,
[curr]: variants[curr].name,
}),
{},
),
}
return this.getFormGroup(
{ [spec.tag.id]: enumSpec, ...spec.variants[selection] },
[],
current,
)
const selectedSpec = selection ? variants[selection].spec : {}
return this.getFormGroup({
['selectedVariant']: enumSpec,
...selectedSpec,
})
}
getListItem(spec: ValueSpecList, entry: any) {
@@ -74,15 +80,13 @@ export class FormService {
return this.formBuilder.control(entry, listItemValidators)
} else if (isValueSpecListOf(spec, 'object')) {
return this.getFormGroup(spec.spec.spec, listItemValidators, entry)
} else if (isValueSpecListOf(spec, 'union')) {
return this.getUnionObject(spec.spec, spec.spec.default, entry)
}
}
private getFormGroup(
config: InputSpec,
validators: ValidatorFn[] = [],
current?: { [key: string]: any } | null,
current?: Record<string, any> | null,
): UntypedFormGroup {
let group: Record<
string,
@@ -129,13 +133,12 @@ export class FormService {
fileValidators(spec),
)
case 'union':
const currentSelection = currentValue?.[spec.tag.id]
const currentSelection = currentValue?.[unionSelectKey]
const isValid = !!spec.variants[currentSelection]
return this.getUnionObject(
spec,
isValid ? currentSelection : spec.default,
isValid ? currentValue : undefined,
)
case 'boolean':
case 'select':
@@ -273,23 +276,20 @@ export function listUnique(spec: ValueSpecList): ValidatorFn {
for (let idx = 0; idx < list.length; idx++) {
for (let idx2 = idx + 1; idx2 < list.length; idx2++) {
if (listItemEquals(spec, list[idx], list[idx2])) {
const objSpec = spec.spec
let display1: string
let display2: string
let uniqueMessage = isObjectOrUnion(spec.spec)
? uniqueByMessageWrapper(
spec.spec['unique-by'],
spec.spec,
list[idx],
)
let uniqueMessage = isObject(objSpec)
? uniqueByMessageWrapper(objSpec.uniqueBy, objSpec, list[idx])
: ''
if (isObjectOrUnion(spec.spec) && spec.spec['display-as']) {
if (isObject(objSpec) && objSpec.displayAs) {
display1 = `"${(Mustache as any).render(
spec.spec['display-as'],
objSpec.displayAs,
list[idx],
)}"`
display2 = `"${(Mustache as any).render(
spec.spec['display-as'],
objSpec.displayAs,
list[idx2],
)}"`
} else {
@@ -318,11 +318,7 @@ function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean {
case 'object':
const obj: ListValueSpecObject = spec.spec as any
return listObjEquals(obj['unique-by'], obj, val1, val2)
case 'union':
const union: ListValueSpecUnion = spec.spec as any
return unionEquals(union['unique-by'], union, val1, val2)
return listObjEquals(obj.uniqueBy, obj, val1, val2)
default:
return false
}
@@ -425,19 +421,18 @@ function objEquals(
function unionEquals(
uniqueBy: UniqueBy,
spec: ValueSpecUnion | ListValueSpecUnion,
spec: ValueSpecUnion,
val1: any,
val2: any,
): boolean {
const tagId = spec.tag.id
const variant = spec.variants[val1[tagId]]
const variantSpec = spec.variants[val1[unionSelectKey]].spec
if (!uniqueBy) {
return false
} else if (typeof uniqueBy === 'string') {
if (uniqueBy === tagId) {
return val1[tagId] === val2[tagId]
if (uniqueBy === unionSelectKey) {
return val1[unionSelectKey] === val2[unionSelectKey]
} else {
return itemEquals(variant[uniqueBy], val1[uniqueBy], val2[uniqueBy])
return itemEquals(variantSpec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
}
} else if ('any' in uniqueBy) {
for (let subSpec of uniqueBy.any) {
@@ -459,19 +454,10 @@ function unionEquals(
function uniqueByMessageWrapper(
uniqueBy: UniqueBy,
spec: ListValueSpecObject | ListValueSpecUnion,
spec: ListValueSpecObject,
obj: Record<string, string>,
) {
let configSpec: InputSpec
if (isUnion(spec)) {
const tagId = spec.tag.id
configSpec = {
[tagId]: { name: spec.tag.name } as ValueSpec,
...spec.variants[obj[tagId]],
}
} else {
configSpec = spec.spec
}
let configSpec = spec.spec
const message = uniqueByMessage(uniqueBy, configSpec)
if (message) {
@@ -509,16 +495,9 @@ function uniqueByMessage(
: '(' + ret + ')'
}
function isObjectOrUnion(
spec: ListValueSpecOf<any>,
): spec is ListValueSpecObject | ListValueSpecUnion {
// only lists of objects and unions have unique-by
return 'unique-by' in spec
}
function isUnion(spec: any): spec is ListValueSpecUnion {
// only unions have tag
return !!spec.tag
function isObject(spec: ListValueSpecOf<any>): spec is ListValueSpecObject {
// only lists of objects have uniqueBy
return 'uniqueBy' in spec
}
export function convertValuesRecursive(
@@ -540,7 +519,8 @@ export function convertValuesRecursive(
convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup)
} else if (valueSpec.type === 'union') {
const formGr = group.get(key) as UntypedFormGroup
const spec = valueSpec.variants[formGr.controls[valueSpec.tag.id].value]
const spec =
valueSpec.variants[formGr.controls[unionSelectKey].value].spec
convertValuesRecursive(spec, formGr)
} else if (valueSpec.type === 'list') {
const formArr = group.get(key) as UntypedFormArray
@@ -559,15 +539,6 @@ export function convertValuesRecursive(
const objectSpec = valueSpec.spec as ListValueSpecObject
convertValuesRecursive(objectSpec.spec, formGroup as UntypedFormGroup)
})
} else if (valueSpec.subtype === 'union') {
controls.forEach(formGroup => {
const unionSpec = valueSpec.spec as ListValueSpecUnion
const spec =
unionSpec.variants[
(formGroup as UntypedFormGroup).controls[unionSpec.tag.id].value
]
convertValuesRecursive(spec, formGroup as UntypedFormGroup)
})
}
}
})

View File

@@ -1,4 +1,4 @@
import { InputSpec } from 'start-sdk/types/config-types'
import { InputSpec } from 'start-sdk/lib/config/config-types'
import { Url } from '@start9labs/shared'
import { Manifest } from '@start9labs/marketplace'
import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info'

View File

@@ -1,4 +1,4 @@
import { DefaultString } from 'start-sdk/types/config-types'
import { DefaultString } from 'start-sdk/lib/config/config-types'
export class Range {
min?: number