diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 73e26c53e..d70c9aa58 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.beta3", + "start-sdk": "^0.4.0-lib0.beta5", "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.beta3", - "resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.beta3.tgz", - "integrity": "sha512-MuKc4QB6rR2UOVZaT3MApJf5jGtIzUmKGLS4mm6Zqvmud+MP/FQTRB8y7CZjyBmBGXQZcSyFUcEBkR/hQTmLXA==", + "version": "0.4.0-lib0.beta5", + "resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.beta5.tgz", + "integrity": "sha512-dHWn7urjTXtS+CujkRp7U/CiSk/mTuvXjmFt8WMhnPExu2FFdmn72LDOICn5hf8kS0oi1qcYtjzq4juD8HlVFQ==", "dependencies": { "@iarna/toml": "^2.2.5", "lodash": "^4.17.21", diff --git a/frontend/package.json b/frontend/package.json index 4655d0eac..7681c4af0 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.beta3", + "start-sdk": "^0.4.0-lib0.beta5", "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 738c29939..4b63da2e4 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 @@ -286,7 +286,6 @@ const CifsSpec: InputSpec = { nullable: false, masked: false, default: null, - textarea: false, warning: null, }, path: { @@ -299,7 +298,6 @@ const CifsSpec: InputSpec = { nullable: false, masked: false, default: null, - textarea: false, warning: null, }, username: { @@ -312,7 +310,6 @@ const CifsSpec: InputSpec = { nullable: false, masked: false, default: null, - textarea: false, warning: null, }, password: { @@ -325,7 +322,6 @@ const CifsSpec: InputSpec = { nullable: true, masked: true, default: null, - textarea: false, warning: 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 639d425bd..6fcc76973 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 @@ -9,7 +9,7 @@ > + @Input() spec!: ValueSpecOf<'string' | 'textarea' | 'number'> @Input() control!: FormControl @Output() onInputChange = new EventEmitter() 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 0ba9b727a..1e0b4d3c6 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 @@ -9,7 +9,11 @@ > +

+ + {{ errors | getError }} + +

@Input() original?: Record + get unionControl(): AbstractControl | null { + return this.formGroup.get(unionSelectKey) + } + get selectedVariant(): string { - return this.formGroup.get(unionSelectKey)?.value + return this.unionControl?.value || '' } get variantName(): string { - return this.spec.variants[this.selectedVariant].name + return this.spec.variants[this.selectedVariant]?.name || '' } get variantSpec(): InputSpec { - return this.spec.variants[this.selectedVariant].spec + return this.spec.variants[this.selectedVariant]?.spec || {} } get hasNewOptions(): boolean { 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 58f53c662..552d77b9f 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 @@ -279,7 +279,6 @@ function getMarketplaceValueSpec(): ValueSpecObject { patternDescription: 'Must be a valid URL', placeholder: 'e.g. https://example.org', default: null, - textarea: false, 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 b006e6caa..c164ce51f 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 @@ -229,7 +229,6 @@ const SAMPLE_CONFIG: InputSpec = { pattern: '^[a-zA-Z0-9! _]+$', patternDescription: 'Must be alphanumeric (may contain underscore).', default: null, - textarea: false, warning: null, }, 'sample-number': { 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 56b318f0a..c38e652bc 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 @@ -30,7 +30,6 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { pattern: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', patternDescription: 'Must be kebab case', default: basicInfo?.id || '', - textarea: false, warning: null, }, title: { @@ -43,7 +42,6 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { pattern: null, patternDescription: null, default: basicInfo ? basicInfo.title : devData.name, - textarea: false, warning: null, }, 'service-version-number': { @@ -57,7 +55,6 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { pattern: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$', patternDescription: 'Must be valid Emver version', default: basicInfo?.['service-version-number'] || '', - textarea: false, warning: null, }, description: { @@ -74,23 +71,17 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { placeholder: null, nullable: false, masked: false, - textarea: true, default: basicInfo?.description?.short || '', pattern: '^.{1,320}$', patternDescription: 'Must be shorter than 320 characters', warning: null, }, long: { - type: 'string', + type: 'textarea', name: 'Long Description', description: `This description will display with additional details in the service's individual marketplace page`, placeholder: null, nullable: false, - masked: false, - textarea: true, - default: basicInfo?.description?.long || '', - pattern: '^.{1,5000}$', - patternDescription: 'Must be shorter than 5000 characters', warning: null, }, }, @@ -105,7 +96,6 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { masked: false, pattern: null, patternDescription: null, - textarea: true, default: basicInfo?.['release-notes'] || '', warning: null, }, @@ -139,7 +129,6 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { nullable: false, masked: false, default: basicInfo?.['wrapper-repo'] || '', - textarea: false, warning: null, }, 'upstream-repo': { @@ -152,7 +141,6 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { nullable: true, masked: false, default: basicInfo?.['upstream-repo'] || '', - textarea: false, warning: null, }, 'support-site': { @@ -165,7 +153,6 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { nullable: true, masked: false, default: basicInfo?.['support-site'] || '', - textarea: false, warning: null, }, 'marketing-site': { @@ -178,7 +165,6 @@ export function getBasicInfoSpec(devData: DevProjectData): InputSpec { nullable: true, masked: false, default: basicInfo?.['marketing-site'] || '', - textarea: false, warning: null, }, } 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 afe928b43..f9aeb7a68 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 @@ -365,7 +365,6 @@ function getWifiValueSpec( nullable: false, masked: false, default: ssid || null, - textarea: false, warning: null, }, password: { @@ -378,7 +377,6 @@ function getWifiValueSpec( pattern: '^.{8,}$', patternDescription: 'Must be longer than 8 characters', default: null, - textarea: false, 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 b923f18bb..d554fcebc 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -13,6 +13,7 @@ import { Manifest, } from '@start9labs/marketplace' import { Log } from '@start9labs/shared' +import { unionSelectKey } from 'start-sdk/lib/config/config-types' export module Mock { export const ServerUpdated: ServerStatusInfo = { @@ -718,7 +719,7 @@ export module Mock { description: '

The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:

  • Bitcoin Core: The Bitcoin Core service installed on this device
  • External Node: A Bitcoin node running on a different device
', warning: null, - default: 'internal', + default: null, nullable: false, variants: { internal: { name: 'Internal', spec: {} }, @@ -734,7 +735,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -781,7 +781,6 @@ export module Mock { pattern: '^[a-zA-Z]+$', patternDescription: 'must contain only letters.', masked: false, - textarea: false, }, rpcuser: { name: 'RPC Username', @@ -794,7 +793,6 @@ export module Mock { pattern: '^[a-zA-Z]+$', patternDescription: 'must contain only letters.', masked: false, - textarea: false, }, rpcpass: { name: 'RPC User Password', @@ -810,7 +808,6 @@ export module Mock { masked: true, pattern: null, patternDescription: null, - textarea: false, }, rpcpass2: { name: 'RPC User Password', @@ -826,12 +823,19 @@ export module Mock { masked: true, pattern: null, patternDescription: null, - textarea: false, }, }, }, }, }, + bio: { + name: 'Bio', + type: 'textarea', + description: 'Your personal bio', + placeholder: 'Tell the world about yourself', + warning: null, + nullable: true, + }, testnet: { name: 'Testnet', type: 'boolean', @@ -882,7 +886,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -899,7 +902,6 @@ export module Mock { patternDescription: 'must contain only letters.', masked: false, placeholder: null, - textarea: false, warning: null, }, age: { @@ -994,7 +996,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1007,7 +1008,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1038,7 +1038,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, }, rulemakerip: { @@ -1052,7 +1051,6 @@ export module Mock { patternDescription: 'may only contain numbers and periods', masked: false, placeholder: null, - textarea: false, warning: null, }, }, @@ -1068,7 +1066,6 @@ export module Mock { patternDescription: 'must contain only letters.', masked: false, placeholder: null, - textarea: false, warning: null, }, rpcpass: { @@ -1084,7 +1081,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, }, }, @@ -1116,7 +1112,6 @@ export module Mock { pattern: '^[a-zA-Z]+$', patternDescription: 'Must contain only letters.', placeholder: null, - textarea: false, warning: null, default: null, }, @@ -1129,7 +1124,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1145,7 +1139,6 @@ export module Mock { patternDescription: 'anything', masked: false, placeholder: null, - textarea: false, warning: null, }, 'private-domain': { @@ -1157,7 +1150,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1188,7 +1180,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1253,7 +1244,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1266,7 +1256,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1279,7 +1268,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1308,7 +1296,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, }, lawagency: { @@ -1323,7 +1310,6 @@ export module Mock { 'may only contain numbers and periods', masked: false, placeholder: null, - textarea: false, warning: null, }, }, @@ -1338,7 +1324,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, default: null, }, @@ -1369,7 +1354,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, }, rulemakerip: { @@ -1383,7 +1367,6 @@ export module Mock { patternDescription: 'may only contain numbers and periods', masked: false, placeholder: null, - textarea: false, warning: null, }, }, @@ -1399,7 +1382,6 @@ export module Mock { patternDescription: 'must contain only letters.', masked: false, placeholder: null, - textarea: false, warning: null, }, rpcpass: { @@ -1415,7 +1397,6 @@ export module Mock { placeholder: null, pattern: null, patternDescription: null, - textarea: false, warning: null, }, }, @@ -1455,7 +1436,7 @@ export module Mock { rulemakers: [], }, 'bitcoin-node': { - type: 'internal', + [unionSelectKey]: 'internal', }, port: 20, rpcallowip: undefined, @@ -1524,7 +1505,6 @@ export module Mock { masked: false, pattern: '^[a-zA-Z]+$', patternDescription: 'Must contain only letters.', - textarea: false, 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 46447a580..b0865e275 100644 --- a/frontend/projects/ui/src/app/services/form.service.ts +++ b/frontend/projects/ui/src/app/services/form.service.ts @@ -25,6 +25,7 @@ import { ValueSpecString, ValueSpecUnion, unionSelectKey, + ValueSpecTextarea, } from 'start-sdk/lib/config/config-types' import { getDefaultString, Range } from '../util/config-utilities' const Mustache = require('mustache') @@ -48,7 +49,7 @@ export class FormService { ): UntypedFormGroup { const { name, description, warning, variants, nullable } = spec - const enumSpec: ValueSpecSelect = { + const selectSpec: ValueSpecSelect = { type: 'select', name, description, @@ -67,7 +68,7 @@ export class FormService { const selectedSpec = selection ? variants[selection].spec : {} return this.getFormGroup({ - ['selectedVariant']: enumSpec, + [unionSelectKey]: selectSpec, ...selectedSpec, }) } @@ -111,6 +112,9 @@ export class FormService { value = spec.default ? getDefaultString(spec.default) : null } return this.formBuilder.control(value, stringValidators(spec)) + case 'textarea': + value = currentValue || null + return this.formBuilder.control(value, textareaValidators(spec)) case 'number': if (currentValue !== undefined) { value = currentValue @@ -141,9 +145,11 @@ export class FormService { isValid ? currentSelection : spec.default, ) case 'boolean': - case 'select': value = currentValue === undefined ? spec.default : currentValue return this.formBuilder.control(value) + case 'select': + value = currentValue === undefined ? spec.default : currentValue + return this.formBuilder.control(value, selectValidators(spec)) case 'multiselect': value = currentValue === undefined ? spec.default : currentValue return this.formBuilder.control(value, multiselectValidators(spec)) @@ -177,6 +183,16 @@ function stringValidators( return validators } +function textareaValidators(spec: ValueSpecTextarea): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (!spec.nullable) { + validators.push(Validators.required) + } + + return validators +} + function numberValidators( spec: ValueSpecNumber | ListValueSpecNumber, ): ValidatorFn[] { @@ -197,6 +213,16 @@ function numberValidators( return validators } +function selectValidators(spec: ValueSpecSelect): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (!spec.nullable) { + validators.push(Validators.required) + } + + return validators +} + function multiselectValidators(spec: ValueSpecMultiselect): ValidatorFn[] { const validators: ValidatorFn[] = [] validators.push(listInRange(spec.range)) @@ -327,6 +353,7 @@ function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean { function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean { switch (spec.type) { case 'string': + case 'textarea': case 'number': case 'boolean': case 'select': @@ -513,7 +540,7 @@ export function convertValuesRecursive( control.setValue( control.value || control.value === 0 ? Number(control.value) : null, ) - } else if (valueSpec.type === 'string') { + } else if (valueSpec.type === 'string' || valueSpec.type === 'textarea') { if (!control.value) control.setValue(null) } else if (valueSpec.type === 'object') { convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup)