From 7909941b70efcf9d2543fdc53e5fb73f002d38bc Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 19 Feb 2026 16:44:44 -0700 Subject: [PATCH] feat: builder-style InputSpec API, prefill plumbing, and port forward fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add addKey() and add() builder methods to InputSpec with InputSpecTools - Move OuterType to last generic param on Value, List, and all dynamic methods - Plumb prefill through getActionInput end-to-end (core → container-runtime → SDK) - Filter port_forwards to enabled addresses only - Bump SDK to 0.4.0-beta.50 --- container-runtime/package-lock.json | 2 +- container-runtime/src/Adapters/RpcListener.ts | 1 + .../Systems/SystemForEmbassy/index.ts | 1 + .../src/Adapters/Systems/SystemForStartOs.ts | 3 +- container-runtime/src/Interfaces/System.ts | 1 + core/src/action.rs | 7 +- core/src/context/rpc.rs | 2 +- core/src/net/host/mod.rs | 2 +- core/src/service/action.rs | 11 +- core/src/service/effects/action.rs | 12 +- core/src/service/mod.rs | 2 +- .../lib/actions/input/builder/inputSpec.ts | 71 ++- .../actions/input/builder/inputSpecTools.ts | 274 +++++++++++ sdk/base/lib/actions/input/builder/list.ts | 56 ++- sdk/base/lib/actions/input/builder/value.ts | 462 ++++++++++-------- .../lib/actions/input/builder/variants.ts | 12 +- sdk/base/lib/actions/setupActions.ts | 6 +- .../lib/osBindings/GetActionInputParams.ts | 6 +- sdk/package/package-lock.json | 4 +- sdk/package/package.json | 2 +- .../services/modals/action-input.component.ts | 1 + 21 files changed, 688 insertions(+), 250 deletions(-) create mode 100644 sdk/base/lib/actions/input/builder/inputSpecTools.ts diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 4106f5877..dfdc22ae3 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -38,7 +38,7 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.49", + "version": "0.4.0-beta.50", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index 5567dd979..5998bc4da 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -437,6 +437,7 @@ export class RpcListener { return system.getActionInput( effects, procedures[2], + input?.prefill ?? null, timeout || null, ) case procedures[1] === "actions" && procedures[3] === "run": diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index cfa02c6ac..7786c4760 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -510,6 +510,7 @@ export class SystemForEmbassy implements System { async getActionInput( effects: Effects, actionId: string, + _prefill: Record | null, timeoutMs: number | null, ): Promise { if (actionId === "config") { diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index 837946ca0..3b0d767ed 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -47,11 +47,12 @@ export class SystemForStartOs implements System { getActionInput( effects: Effects, id: string, + prefill: Record | null, timeoutMs: number | null, ): Promise { const action = this.abi.actions.get(id) if (!action) throw new Error(`Action ${id} not found`) - return action.getInput({ effects }) + return action.getInput({ effects, prefill }) } runAction( effects: Effects, diff --git a/container-runtime/src/Interfaces/System.ts b/container-runtime/src/Interfaces/System.ts index 1bda6afd7..a6b9d4fef 100644 --- a/container-runtime/src/Interfaces/System.ts +++ b/container-runtime/src/Interfaces/System.ts @@ -33,6 +33,7 @@ export type System = { getActionInput( effects: Effects, actionId: string, + prefill: Record | null, timeoutMs: number | null, ): Promise diff --git a/core/src/action.rs b/core/src/action.rs index 2d3e888ce..5fa65c961 100644 --- a/core/src/action.rs +++ b/core/src/action.rs @@ -67,6 +67,10 @@ pub struct GetActionInputParams { pub package_id: PackageId, #[arg(help = "help.arg.action-id")] pub action_id: ActionId, + #[ts(type = "Record | null")] + #[serde(default)] + #[arg(skip)] + pub prefill: Option, } #[instrument(skip_all)] @@ -75,6 +79,7 @@ pub async fn get_action_input( GetActionInputParams { package_id, action_id, + prefill, }: GetActionInputParams, ) -> Result, Error> { ctx.services @@ -82,7 +87,7 @@ pub async fn get_action_input( .await .as_ref() .or_not_found(lazy_format!("Manager for {}", package_id))? - .get_action_input(Guid::new(), action_id) + .get_action_input(Guid::new(), action_id, prefill.unwrap_or(Value::Null)) .await } diff --git a/core/src/context/rpc.rs b/core/src/context/rpc.rs index 6350672f1..7a4e6cfb9 100644 --- a/core/src/context/rpc.rs +++ b/core/src/context/rpc.rs @@ -533,7 +533,7 @@ impl RpcContext { for (package_id, action_id) in tasks { if let Some(service) = self.services.get(&package_id).await.as_ref() { if let Some(input) = service - .get_action_input(procedure_id.clone(), action_id.clone()) + .get_action_input(procedure_id.clone(), action_id.clone(), Value::Null) .await .log_err() .flatten() diff --git a/core/src/net/host/mod.rs b/core/src/net/host/mod.rs index 3454d4735..aa23bdaca 100644 --- a/core/src/net/host/mod.rs +++ b/core/src/net/host/mod.rs @@ -298,7 +298,7 @@ impl Model { let bindings: Bindings = this.bindings.de()?; let mut port_forwards = BTreeSet::new(); for bind in bindings.values() { - for addr in &bind.addresses.available { + for addr in bind.addresses.enabled() { if !addr.public { continue; } diff --git a/core/src/service/action.rs b/core/src/service/action.rs index 6bb69bf1d..b00f654ad 100644 --- a/core/src/service/action.rs +++ b/core/src/service/action.rs @@ -17,6 +17,7 @@ use crate::{ActionId, PackageId, ReplayId}; pub(super) struct GetActionInput { id: ActionId, + prefill: Value, } impl Handler for ServiceActor { type Response = Result, Error>; @@ -26,7 +27,10 @@ impl Handler for ServiceActor { async fn handle( &mut self, id: Guid, - GetActionInput { id: action_id }: GetActionInput, + GetActionInput { + id: action_id, + prefill, + }: GetActionInput, _: &BackgroundJobQueue, ) -> Self::Response { let container = &self.0.persistent_container; @@ -34,7 +38,7 @@ impl Handler for ServiceActor { .execute::>( id, ProcedureName::GetActionInput(action_id), - Value::Null, + json!({ "prefill": prefill }), Some(Duration::from_secs(30)), ) .await @@ -47,6 +51,7 @@ impl Service { &self, id: Guid, action_id: ActionId, + prefill: Value, ) -> Result, Error> { if !self .seed @@ -67,7 +72,7 @@ impl Service { return Ok(None); } self.actor - .send(id, GetActionInput { id: action_id }) + .send(id, GetActionInput { id: action_id, prefill }) .await? } } diff --git a/core/src/service/effects/action.rs b/core/src/service/effects/action.rs index d157eafe5..1aa0ab5bd 100644 --- a/core/src/service/effects/action.rs +++ b/core/src/service/effects/action.rs @@ -122,6 +122,10 @@ pub struct GetActionInputParams { package_id: Option, #[arg(help = "help.arg.action-id")] action_id: ActionId, + #[ts(type = "Record | null")] + #[serde(default)] + #[arg(skip)] + prefill: Option, } async fn get_action_input( context: EffectContext, @@ -129,9 +133,11 @@ async fn get_action_input( procedure_id, package_id, action_id, + prefill, }: GetActionInputParams, ) -> Result, Error> { let context = context.deref()?; + let prefill = prefill.unwrap_or(Value::Null); if let Some(package_id) = package_id { context @@ -142,10 +148,10 @@ async fn get_action_input( .await .as_ref() .or_not_found(&package_id)? - .get_action_input(procedure_id, action_id) + .get_action_input(procedure_id, action_id, prefill) .await } else { - context.get_action_input(procedure_id, action_id).await + context.get_action_input(procedure_id, action_id, prefill).await } } @@ -245,7 +251,7 @@ async fn create_task( .as_ref() { let Some(prev) = service - .get_action_input(procedure_id.clone(), task.action_id.clone()) + .get_action_input(procedure_id.clone(), task.action_id.clone(), Value::Null) .await? else { return Err(Error::new( diff --git a/core/src/service/mod.rs b/core/src/service/mod.rs index 255376c5b..0e2cde585 100644 --- a/core/src/service/mod.rs +++ b/core/src/service/mod.rs @@ -534,7 +534,7 @@ impl Service { .contains_key(&action_id)? { if let Some(input) = service - .get_action_input(procedure_id.clone(), action_id.clone()) + .get_action_input(procedure_id.clone(), action_id.clone(), Value::Null) .await .log_err() .flatten() diff --git a/sdk/base/lib/actions/input/builder/inputSpec.ts b/sdk/base/lib/actions/input/builder/inputSpec.ts index 986d898f1..8e76ff99b 100644 --- a/sdk/base/lib/actions/input/builder/inputSpec.ts +++ b/sdk/base/lib/actions/input/builder/inputSpec.ts @@ -4,12 +4,14 @@ import { _ } from '../../../util' import { Effects } from '../../../Effects' import { Parser, object } from 'ts-matches' import { DeepPartial } from '../../../types' +import { InputSpecTools, createInputSpecTools } from './inputSpecTools' -export type LazyBuildOptions = { +export type LazyBuildOptions = { effects: Effects + prefill: DeepPartial | null } -export type LazyBuild = ( - options: LazyBuildOptions, +export type LazyBuild = ( + options: LazyBuildOptions, ) => Promise | ExpectedOut // prettier-ignore @@ -29,7 +31,7 @@ export type InputSpecOf> = { [K in keyof A]: Value } -export type MaybeLazyValues = LazyBuild | A +export type MaybeLazyValues = LazyBuild | A /** * InputSpecs are the specs that are used by the os input specification form for this service. * Here is an example of a simple input specification @@ -98,7 +100,7 @@ export class InputSpec< ) {} public _TYPE: Type = null as any as Type public _PARTIAL: DeepPartial = null as any as DeepPartial - async build(options: LazyBuildOptions): Promise<{ + async build(options: LazyBuildOptions): Promise<{ spec: { [K in keyof Type]: ValueSpec } @@ -121,6 +123,57 @@ export class InputSpec< } } + addKey>( + key: Key, + build: V | ((tools: InputSpecTools) => V), + ): InputSpec< + Type & { [K in Key]: V extends Value ? T : never }, + StaticValidatedAs & { + [K in Key]: V extends Value ? S : never + } + > { + const value = + build instanceof Function ? build(createInputSpecTools()) : build + const newSpec = { ...this.spec, [key]: value } as any + const newValidator = object( + Object.fromEntries( + Object.entries(newSpec).map(([k, v]) => [ + k, + (v as Value).validator, + ]), + ), + ) + return new InputSpec(newSpec, newValidator as any) + } + + add>>( + build: AddSpec | ((tools: InputSpecTools) => AddSpec), + ): InputSpec< + Type & { + [K in keyof AddSpec]: AddSpec[K] extends Value + ? T + : never + }, + StaticValidatedAs & { + [K in keyof AddSpec]: AddSpec[K] extends Value + ? S + : never + } + > { + const addedValues = + build instanceof Function ? build(createInputSpecTools()) : build + const newSpec = { ...this.spec, ...addedValues } as any + const newValidator = object( + Object.fromEntries( + Object.entries(newSpec).map(([k, v]) => [ + k, + (v as Value).validator, + ]), + ), + ) + return new InputSpec(newSpec, newValidator as any) + } + static of>>(spec: Spec) { const validator = object( Object.fromEntries( @@ -129,10 +182,14 @@ export class InputSpec< ) return new InputSpec< { - [K in keyof Spec]: Spec[K] extends Value ? T : never + [K in keyof Spec]: Spec[K] extends Value + ? T + : never }, { - [K in keyof Spec]: Spec[K] extends Value ? T : never + [K in keyof Spec]: Spec[K] extends Value + ? T + : never } >(spec, validator as any) } diff --git a/sdk/base/lib/actions/input/builder/inputSpecTools.ts b/sdk/base/lib/actions/input/builder/inputSpecTools.ts new file mode 100644 index 000000000..d3eed62e9 --- /dev/null +++ b/sdk/base/lib/actions/input/builder/inputSpecTools.ts @@ -0,0 +1,274 @@ +import { InputSpec, LazyBuild } from './inputSpec' +import { AsRequired, FileInfo, Value } from './value' +import { List } from './list' +import { UnionRes, UnionResStaticValidatedAs, Variants } from './variants' +import { + Pattern, + RandomString, + ValueSpecDatetime, + ValueSpecText, +} from '../inputSpecTypes' +import { DefaultString } from '../inputSpecTypes' +import { Parser } from 'ts-matches' +import { ListValueSpecText } from '../inputSpecTypes' + +export interface InputSpecTools { + Value: BoundValue + Variants: typeof Variants + InputSpec: typeof InputSpec + List: BoundList +} + +export interface BoundValue { + // Static (non-dynamic) methods — no OuterType involved + toggle: typeof Value.toggle + text: typeof Value.text + textarea: typeof Value.textarea + number: typeof Value.number + color: typeof Value.color + datetime: typeof Value.datetime + select: typeof Value.select + multiselect: typeof Value.multiselect + object: typeof Value.object + file: typeof Value.file + list: typeof Value.list + hidden: typeof Value.hidden + union: typeof Value.union + + // Dynamic methods with OuterType pre-bound (last generic param removed) + dynamicToggle( + a: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: boolean + disabled?: false | string + }, + OuterType + >, + ): Value + + dynamicText( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: DefaultString | null + required: Required + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + inputmode?: ValueSpecText['inputmode'] + disabled?: string | false + generate?: null | RandomString + }, + OuterType + >, + ): Value, string | null, OuterType> + + dynamicTextarea( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: Required + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + minRows?: number + maxRows?: number + placeholder?: string | null + disabled?: false | string + }, + OuterType + >, + ): Value, string | null, OuterType> + + dynamicNumber( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: number | null + required: Required + min?: number | null + max?: number | null + step?: number | null + integer: boolean + units?: string | null + placeholder?: string | null + disabled?: false | string + }, + OuterType + >, + ): Value, number | null, OuterType> + + dynamicColor( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: Required + disabled?: false | string + }, + OuterType + >, + ): Value, string | null, OuterType> + + dynamicDatetime( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: Required + inputmode?: ValueSpecDatetime['inputmode'] + min?: string | null + max?: string | null + disabled?: false | string + }, + OuterType + >, + ): Value, string | null, OuterType> + + dynamicSelect>( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string + values: Values + disabled?: false | string | string[] + }, + OuterType + >, + ): Value + + dynamicMultiselect>( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string[] + values: Values + minLength?: number | null + maxLength?: number | null + disabled?: false | string | string[] + }, + OuterType + >, + ): Value<(keyof Values & string)[], (keyof Values & string)[], OuterType> + + dynamicFile( + a: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + extensions: string[] + required: Required + }, + OuterType + >, + ): Value, FileInfo | null, OuterType> + + dynamicUnion< + VariantValues extends { + [K in string]: { + name: string + spec: InputSpec + } + }, + >( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + variants: Variants + default: keyof VariantValues & string + disabled: string[] | false | string + }, + OuterType + >, + ): Value, UnionRes, OuterType> + dynamicUnion< + StaticVariantValues extends { + [K in string]: { + name: string + spec: InputSpec + } + }, + VariantValues extends StaticVariantValues, + >( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + variants: Variants + default: keyof VariantValues & string + disabled: string[] | false | string + }, + OuterType + >, + validator: Parser>, + ): Value< + UnionRes, + UnionResStaticValidatedAs, + OuterType + > + + dynamicHidden( + getParser: LazyBuild, OuterType>, + ): Value +} + +export interface BoundList { + text: typeof List.text + obj: typeof List.obj + dynamicText( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default?: string[] + minLength?: number | null + maxLength?: number | null + disabled?: false | string + generate?: null | RandomString + spec: { + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + inputmode?: ListValueSpecText['inputmode'] + } + }, + OuterType + >, + ): List +} + +export function createInputSpecTools(): InputSpecTools { + return { + Value: Value as any as BoundValue, + Variants, + InputSpec, + List: List as any as BoundList, + } +} diff --git a/sdk/base/lib/actions/input/builder/list.ts b/sdk/base/lib/actions/input/builder/list.ts index 765775086..89663b011 100644 --- a/sdk/base/lib/actions/input/builder/list.ts +++ b/sdk/base/lib/actions/input/builder/list.ts @@ -9,12 +9,19 @@ import { } from '../inputSpecTypes' import { Parser, arrayOf, string } from 'ts-matches' -export class List { +export class List< + Type extends StaticValidatedAs, + StaticValidatedAs = Type, + OuterType = unknown, +> { private constructor( - public build: LazyBuild<{ - spec: ValueSpecList - validator: Parser - }>, + public build: LazyBuild< + { + spec: ValueSpecList + validator: Parser + }, + OuterType + >, public readonly validator: Parser, ) {} readonly _TYPE: Type = null as any @@ -90,28 +97,31 @@ export class List { }, validator) } - static dynamicText( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default?: string[] - minLength?: number | null - maxLength?: number | null - disabled?: false | string - generate?: null | RandomString - spec: { - masked?: boolean - placeholder?: string | null + static dynamicText( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default?: string[] minLength?: number | null maxLength?: number | null - patterns?: Pattern[] - inputmode?: ListValueSpecText['inputmode'] - } - }>, + disabled?: false | string + generate?: null | RandomString + spec: { + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + inputmode?: ListValueSpecText['inputmode'] + } + }, + OuterType + >, ) { const validator = arrayOf(string) - return new List(async (options) => { + return new List(async (options) => { const { spec: aSpec, ...a } = await getA(options) const spec = { type: 'text' as const, diff --git a/sdk/base/lib/actions/input/builder/value.ts b/sdk/base/lib/actions/input/builder/value.ts index b211376cc..927173c56 100644 --- a/sdk/base/lib/actions/input/builder/value.ts +++ b/sdk/base/lib/actions/input/builder/value.ts @@ -32,7 +32,7 @@ export const fileInfoParser = object({ }) export type FileInfo = typeof fileInfoParser._TYPE -type AsRequired = Required extends true +export type AsRequired = Required extends true ? T : T | null @@ -47,12 +47,19 @@ function asRequiredParser( return parser.nullable() as any } -export class Value { +export class Value< + Type extends StaticValidatedAs, + StaticValidatedAs = Type, + OuterType = unknown, +> { protected constructor( - public build: LazyBuild<{ - spec: ValueSpec - validator: Parser - }>, + public build: LazyBuild< + { + spec: ValueSpec + validator: Parser + }, + OuterType + >, public readonly validator: Parser, ) {} public _TYPE: Type = null as any as Type @@ -102,17 +109,20 @@ export class Value { validator, ) } - static dynamicToggle( - a: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default: boolean - disabled?: false | string - }>, + static dynamicToggle( + a: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: boolean + disabled?: false | string + }, + OuterType + >, ) { const validator = boolean - return new Value( + return new Value( async (options) => ({ spec: { description: null, @@ -225,24 +235,27 @@ export class Value { validator, ) } - static dynamicText( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default: DefaultString | null - required: Required - masked?: boolean - placeholder?: string | null - minLength?: number | null - maxLength?: number | null - patterns?: Pattern[] - inputmode?: ValueSpecText['inputmode'] - disabled?: string | false - generate?: null | RandomString - }>, + static dynamicText( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: DefaultString | null + required: Required + masked?: boolean + placeholder?: string | null + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + inputmode?: ValueSpecText['inputmode'] + disabled?: string | false + generate?: null | RandomString + }, + OuterType + >, ) { - return new Value, string | null>( + return new Value, string | null, OuterType>( async (options) => { const a = await getA(options) return { @@ -342,23 +355,26 @@ export class Value { return { spec: built, validator } }, validator) } - static dynamicTextarea( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default: string | null - required: Required - minLength?: number | null - maxLength?: number | null - patterns?: Pattern[] - minRows?: number - maxRows?: number - placeholder?: string | null - disabled?: false | string - }>, + static dynamicTextarea( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: Required + minLength?: number | null + maxLength?: number | null + patterns?: Pattern[] + minRows?: number + maxRows?: number + placeholder?: string | null + disabled?: false | string + }, + OuterType + >, ) { - return new Value, string | null>( + return new Value, string | null, OuterType>( async (options) => { const a = await getA(options) return { @@ -461,23 +477,26 @@ export class Value { validator, ) } - static dynamicNumber( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default: number | null - required: Required - min?: number | null - max?: number | null - step?: number | null - integer: boolean - units?: string | null - placeholder?: string | null - disabled?: false | string - }>, + static dynamicNumber( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: number | null + required: Required + min?: number | null + max?: number | null + step?: number | null + integer: boolean + units?: string | null + placeholder?: string | null + disabled?: false | string + }, + OuterType + >, ) { - return new Value, number | null>( + return new Value, number | null, OuterType>( async (options) => { const a = await getA(options) return { @@ -553,17 +572,20 @@ export class Value { ) } - static dynamicColor( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default: string | null - required: Required - disabled?: false | string - }>, + static dynamicColor( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: Required + disabled?: false | string + }, + OuterType + >, ) { - return new Value, string | null>( + return new Value, string | null, OuterType>( async (options) => { const a = await getA(options) return { @@ -647,20 +669,23 @@ export class Value { validator, ) } - static dynamicDatetime( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default: string | null - required: Required - inputmode?: ValueSpecDatetime['inputmode'] - min?: string | null - max?: string | null - disabled?: false | string - }>, + static dynamicDatetime( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string | null + required: Required + inputmode?: ValueSpecDatetime['inputmode'] + min?: string | null + max?: string | null + disabled?: false | string + }, + OuterType + >, ) { - return new Value, string | null>( + return new Value, string | null, OuterType>( async (options) => { const a = await getA(options) return { @@ -750,34 +775,43 @@ export class Value { validator, ) } - static dynamicSelect>( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default: string - values: Values - disabled?: false | string | string[] - }>, + static dynamicSelect< + Values extends Record, + OuterType = unknown, + >( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string + values: Values + disabled?: false | string | string[] + }, + OuterType + >, ) { - return new Value(async (options) => { - const a = await getA(options) - return { - spec: { - description: null, - warning: null, - type: 'select' as const, - disabled: false, - immutable: false, - ...a, - }, - validator: anyOf( - ...Object.keys(a.values).map((x: keyof Values & string) => - literal(x), + return new Value( + async (options) => { + const a = await getA(options) + return { + spec: { + description: null, + warning: null, + type: 'select' as const, + disabled: false, + immutable: false, + ...a, + }, + validator: anyOf( + ...Object.keys(a.values).map((x: keyof Values & string) => + literal(x), + ), ), - ), - } - }, string) + } + }, + string, + ) } /** * @description Displays a select modal with checkboxes, allowing for multiple selections. @@ -851,19 +885,29 @@ export class Value { validator, ) } - static dynamicMultiselect>( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - default: string[] - values: Values - minLength?: number | null - maxLength?: number | null - disabled?: false | string | string[] - }>, + static dynamicMultiselect< + Values extends Record, + OuterType = unknown, + >( + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + default: string[] + values: Values + minLength?: number | null + maxLength?: number | null + disabled?: false | string | string[] + }, + OuterType + >, ) { - return new Value<(keyof Values & string)[], string[]>(async (options) => { + return new Value< + (keyof Values & string)[], + (keyof Values & string)[], + OuterType + >(async (options) => { const a = await getA(options) return { spec: { @@ -948,30 +992,34 @@ export class Value { asRequiredParser(fileInfoParser, a), ) } - static dynamicFile( - a: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - extensions: string[] - required: Required - }>, - ) { - return new Value, FileInfo | null>( - async (options) => { - const spec = { - type: 'file' as const, - description: null, - warning: null, - ...(await a(options)), - } - return { - spec, - validator: asRequiredParser(fileInfoParser, spec), - } + static dynamicFile( + a: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + extensions: string[] + required: Required }, - fileInfoParser.nullable(), - ) + OuterType + >, + ) { + return new Value< + AsRequired, + FileInfo | null, + OuterType + >(async (options) => { + const spec = { + type: 'file' as const, + description: null, + warning: null, + ...(await a(options)), + } + return { + spec, + validator: asRequiredParser(fileInfoParser, spec), + } + }, fileInfoParser.nullable()) } /** * @description Displays a dropdown, allowing for a single selection. Depending on the selection, a different object ("sub form") is presented. @@ -1053,37 +1101,46 @@ export class Value { spec: InputSpec } }, + OuterType = unknown, >( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - variants: Variants - default: keyof VariantValues & string - disabled: string[] | false | string - }>, - ): Value, unknown> + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + variants: Variants + default: keyof VariantValues & string + disabled: string[] | false | string + }, + OuterType + >, + ): Value, UnionRes, OuterType> static dynamicUnion< - VariantValues extends StaticVariantValues, StaticVariantValues extends { [K in string]: { name: string spec: InputSpec } }, + VariantValues extends StaticVariantValues, + OuterType = unknown, >( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - variants: Variants - default: keyof VariantValues & string - disabled: string[] | false | string - }>, + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + variants: Variants + default: keyof VariantValues & string + disabled: string[] | false | string + }, + OuterType + >, validator: Parser>, ): Value< UnionRes, - UnionResStaticValidatedAs + UnionResStaticValidatedAs, + OuterType > static dynamicUnion< VariantValues extends { @@ -1092,35 +1149,40 @@ export class Value { spec: InputSpec } }, + OuterType = unknown, >( - getA: LazyBuild<{ - name: string - description?: string | null - warning?: string | null - variants: Variants - default: keyof VariantValues & string - disabled: string[] | false | string - }>, + getA: LazyBuild< + { + name: string + description?: string | null + warning?: string | null + variants: Variants + default: keyof VariantValues & string + disabled: string[] | false | string + }, + OuterType + >, validator: Parser = any, ) { - return new Value, typeof validator._TYPE>( - async (options) => { - const newValues = await getA(options) - const built = await newValues.variants.build(options as any) - return { - spec: { - type: 'union' as const, - description: null, - warning: null, - ...newValues, - variants: built.spec, - immutable: false, - }, - validator: built.validator, - } - }, - validator, - ) + return new Value< + UnionRes, + typeof validator._TYPE, + OuterType + >(async (options) => { + const newValues = await getA(options) + const built = await newValues.variants.build(options as any) + return { + spec: { + type: 'union' as const, + description: null, + warning: null, + ...newValues, + variants: built.spec, + immutable: false, + }, + validator: built.validator, + } + }, validator) } /** * @description Presents an interface to add/remove/edit items in a list. @@ -1196,7 +1258,7 @@ export class Value { hiddenExample: Value.hidden(), * ``` */ - static hidden(): Value + static hidden(): Value static hidden(parser: Parser): Value static hidden(parser: Parser = any) { return new Value(async () => { @@ -1216,8 +1278,10 @@ export class Value { hiddenExample: Value.hidden(), * ``` */ - static dynamicHidden(getParser: LazyBuild>) { - return new Value(async (options) => { + static dynamicHidden( + getParser: LazyBuild, OuterType>, + ) { + return new Value(async (options) => { const validator = await getParser(options) return { spec: { @@ -1228,9 +1292,9 @@ export class Value { }, any) } - map(fn: (value: StaticValidatedAs) => U): Value { - return new Value(async (effects) => { - const built = await this.build(effects) + map(fn: (value: StaticValidatedAs) => U): Value { + return new Value(async (options) => { + const built = await this.build(options) return { spec: built.spec, validator: built.validator.map(fn), diff --git a/sdk/base/lib/actions/input/builder/variants.ts b/sdk/base/lib/actions/input/builder/variants.ts index 46818d893..3f9be0ec4 100644 --- a/sdk/base/lib/actions/input/builder/variants.ts +++ b/sdk/base/lib/actions/input/builder/variants.ts @@ -103,12 +103,16 @@ export class Variants< spec: InputSpec } }, + OuterType = unknown, > { private constructor( - public build: LazyBuild<{ - spec: ValueSpecUnion['variants'] - validator: Parser> - }>, + public build: LazyBuild< + { + spec: ValueSpecUnion['variants'] + validator: Parser> + }, + OuterType + >, public readonly validator: Parser< unknown, UnionResStaticValidatedAs diff --git a/sdk/base/lib/actions/setupActions.ts b/sdk/base/lib/actions/setupActions.ts index d056be56e..e648e6322 100644 --- a/sdk/base/lib/actions/setupActions.ts +++ b/sdk/base/lib/actions/setupActions.ts @@ -13,6 +13,7 @@ export type Run> = (options: { }) => Promise<(T.ActionResult & { version: '1' }) | null | void | undefined> export type GetInput> = (options: { effects: T.Effects + prefill: T.DeepPartial | null }) => Promise> export type MaybeFn = T | ((options: { effects: T.Effects }) => Promise) @@ -104,7 +105,10 @@ export class Action> await options.effects.action.export({ id: this.id, metadata }) return metadata } - async getInput(options: { effects: T.Effects }): Promise { + async getInput(options: { + effects: T.Effects + prefill: T.DeepPartial | null + }): Promise { let spec = {} if (this.inputSpec) { const built = await this.inputSpec.build(options) diff --git a/sdk/base/lib/osBindings/GetActionInputParams.ts b/sdk/base/lib/osBindings/GetActionInputParams.ts index 3142ca2f0..d38727ca8 100644 --- a/sdk/base/lib/osBindings/GetActionInputParams.ts +++ b/sdk/base/lib/osBindings/GetActionInputParams.ts @@ -2,4 +2,8 @@ import type { ActionId } from './ActionId' import type { PackageId } from './PackageId' -export type GetActionInputParams = { packageId?: PackageId; actionId: ActionId } +export type GetActionInputParams = { + packageId?: PackageId + actionId: ActionId + prefill: Record | null +} diff --git a/sdk/package/package-lock.json b/sdk/package/package-lock.json index 6ff6106c6..5024774c6 100644 --- a/sdk/package/package-lock.json +++ b/sdk/package/package-lock.json @@ -1,12 +1,12 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.49", + "version": "0.4.0-beta.50", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.49", + "version": "0.4.0-beta.50", "license": "MIT", "dependencies": { "@iarna/toml": "^3.0.0", diff --git a/sdk/package/package.json b/sdk/package/package.json index b105f59ee..4583158b0 100644 --- a/sdk/package/package.json +++ b/sdk/package/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-beta.49", + "version": "0.4.0-beta.50", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./package/lib/index.js", "types": "./package/lib/index.d.ts", diff --git a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts index cbcbbf758..dcf265ae4 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/modals/action-input.component.ts @@ -148,6 +148,7 @@ export class ActionInputModal { this.api.getActionInput({ packageId: this.pkgInfo.id, actionId: this.actionId, + prefill: this.context.data.prefill ?? null, }), ).pipe( map(res => {