mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
add comments to everything potentially consumer facing (#3127)
* add comments to everything potentially consumer facing * rework smtp --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
@@ -6,19 +6,28 @@ import { z } from 'zod'
|
||||
import { DeepPartial } from '../../../types'
|
||||
import { InputSpecTools, createInputSpecTools } from './inputSpecTools'
|
||||
|
||||
/** Options passed to a lazy builder function when resolving dynamic form field values. */
|
||||
export type LazyBuildOptions<Type> = {
|
||||
/** The effects interface for runtime operations (e.g. reading files, querying state). */
|
||||
effects: Effects
|
||||
/** Previously saved form data to pre-fill the form with, or `null` for fresh creation. */
|
||||
prefill: DeepPartial<Type> | null
|
||||
}
|
||||
/**
|
||||
* A function that lazily produces a value, potentially using effects and prefill data.
|
||||
* Used by `dynamic*` variants of {@link Value} to compute form field options at runtime.
|
||||
*/
|
||||
export type LazyBuild<ExpectedOut, Type> = (
|
||||
options: LazyBuildOptions<Type>,
|
||||
) => Promise<ExpectedOut> | ExpectedOut
|
||||
|
||||
/** Extracts the runtime type from an {@link InputSpec}. */
|
||||
// prettier-ignore
|
||||
export type ExtractInputSpecType<A extends InputSpec<Record<string, any>, any>> =
|
||||
export type ExtractInputSpecType<A extends InputSpec<Record<string, any>, any>> =
|
||||
A extends InputSpec<infer B, any> ? B :
|
||||
never
|
||||
|
||||
/** Extracts the static validation type from an {@link InputSpec}. */
|
||||
export type ExtractInputSpecStaticValidatedAs<
|
||||
A extends InputSpec<any, Record<string, any>>,
|
||||
> = A extends InputSpec<any, infer B> ? B : never
|
||||
@@ -27,10 +36,12 @@ export type ExtractInputSpecStaticValidatedAs<
|
||||
// A extends Record<string, any> | InputSpec<Record<string, any>>,
|
||||
// > = A extends InputSpec<infer B> ? DeepPartial<B> : DeepPartial<A>
|
||||
|
||||
/** Maps an object type to a record of {@link Value} entries for use with `InputSpec.of`. */
|
||||
export type InputSpecOf<A extends Record<string, any>> = {
|
||||
[K in keyof A]: Value<A[K]>
|
||||
}
|
||||
|
||||
/** A value that is either directly provided or lazily computed via a {@link LazyBuild} function. */
|
||||
export type MaybeLazyValues<A, T> = LazyBuild<A, T> | A
|
||||
/**
|
||||
* InputSpecs are the specs that are used by the os input specification form for this service.
|
||||
@@ -100,6 +111,11 @@ export class InputSpec<
|
||||
) {}
|
||||
public _TYPE: Type = null as any as Type
|
||||
public _PARTIAL: DeepPartial<Type> = null as any as DeepPartial<Type>
|
||||
/**
|
||||
* Builds the runtime form specification and combined Zod validator from this InputSpec's fields.
|
||||
*
|
||||
* @returns An object containing the resolved `spec` (field specs keyed by name) and a combined `validator`
|
||||
*/
|
||||
async build<OuterType>(options: LazyBuildOptions<OuterType>): Promise<{
|
||||
spec: {
|
||||
[K in keyof Type]: ValueSpec
|
||||
@@ -123,6 +139,12 @@ export class InputSpec<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single named field to this spec, returning a new `InputSpec` with the extended type.
|
||||
*
|
||||
* @param key - The field key name
|
||||
* @param build - A {@link Value} instance, or a function receiving typed tools that returns one
|
||||
*/
|
||||
addKey<Key extends string, V extends Value<any, any, any>>(
|
||||
key: Key,
|
||||
build: V | ((tools: InputSpecTools<Type>) => V),
|
||||
@@ -146,6 +168,11 @@ export class InputSpec<
|
||||
return new InputSpec(newSpec, newValidator as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple fields to this spec at once, returning a new `InputSpec` with extended types.
|
||||
*
|
||||
* @param build - A record of {@link Value} entries, or a function receiving typed tools that returns one
|
||||
*/
|
||||
add<AddSpec extends Record<string, Value<any, any, any>>>(
|
||||
build: AddSpec | ((tools: InputSpecTools<Type>) => AddSpec),
|
||||
): InputSpec<
|
||||
@@ -174,6 +201,17 @@ export class InputSpec<
|
||||
return new InputSpec(newSpec, newValidator as any)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an `InputSpec` from a plain record of {@link Value} entries.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const spec = InputSpec.of({
|
||||
* username: Value.text({ name: 'Username', required: true, default: null }),
|
||||
* verbose: Value.toggle({ name: 'Verbose Logging', default: false }),
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
static of<Spec extends Record<string, Value<any, any>>>(spec: Spec) {
|
||||
const validator = z.object(
|
||||
Object.fromEntries(
|
||||
|
||||
@@ -9,6 +9,14 @@ import {
|
||||
} from '../inputSpecTypes'
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* Builder class for defining list-type form fields.
|
||||
*
|
||||
* A list presents an interface to add, remove, and reorder items. Items can be
|
||||
* either text strings ({@link List.text}) or structured objects ({@link List.obj}).
|
||||
*
|
||||
* Used with {@link Value.list} to include a list field in an {@link InputSpec}.
|
||||
*/
|
||||
export class List<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs = Type,
|
||||
@@ -26,6 +34,12 @@ export class List<
|
||||
) {}
|
||||
readonly _TYPE: Type = null as any
|
||||
|
||||
/**
|
||||
* Creates a list of text input items.
|
||||
*
|
||||
* @param a - List-level options (name, description, min/max length, defaults)
|
||||
* @param aSpec - Item-level options (patterns, input mode, masking, generation)
|
||||
*/
|
||||
static text(
|
||||
a: {
|
||||
name: string
|
||||
@@ -97,6 +111,7 @@ export class List<
|
||||
}, validator)
|
||||
}
|
||||
|
||||
/** Like {@link List.text} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicText<OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
@@ -150,6 +165,12 @@ export class List<
|
||||
}, validator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of structured object items, each defined by a nested {@link InputSpec}.
|
||||
*
|
||||
* @param a - List-level options (name, description, min/max length)
|
||||
* @param aSpec - Item-level options (the nested spec, display expression, uniqueness constraint)
|
||||
*/
|
||||
static obj<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs extends Record<string, any>,
|
||||
|
||||
@@ -15,12 +15,15 @@ import { _, once } from '../../../util'
|
||||
import { z } from 'zod'
|
||||
import { DeepPartial } from '../../../types'
|
||||
|
||||
/** Zod schema for a file upload result — validates `{ path, commitment: { hash, size } }`. */
|
||||
export const fileInfoParser = z.object({
|
||||
path: z.string(),
|
||||
commitment: z.object({ hash: z.string(), size: z.number() }),
|
||||
})
|
||||
/** The parsed result of a file upload, containing the file path and its content commitment (hash + size). */
|
||||
export type FileInfo = z.infer<typeof fileInfoParser>
|
||||
|
||||
/** Conditional type: returns `T` if `Required` is `true`, otherwise `T | null`. */
|
||||
export type AsRequired<T, Required extends boolean> = Required extends true
|
||||
? T
|
||||
: T | null
|
||||
@@ -37,6 +40,19 @@ function asRequiredParser<Type, Input extends { required: boolean }>(
|
||||
return parser.nullable() as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Core builder class for defining a single form field in a service configuration spec.
|
||||
*
|
||||
* Each static factory method (e.g. `Value.text()`, `Value.toggle()`, `Value.select()`) creates
|
||||
* a typed `Value` instance representing a specific field type. Dynamic variants (e.g. `Value.dynamicText()`)
|
||||
* allow the field options to be computed lazily at runtime.
|
||||
*
|
||||
* Use with {@link InputSpec} to compose complete form specifications.
|
||||
*
|
||||
* @typeParam Type - The runtime type this field produces when filled in
|
||||
* @typeParam StaticValidatedAs - The compile-time validated type (usually same as Type)
|
||||
* @typeParam OuterType - The parent form's type context (used by dynamic variants)
|
||||
*/
|
||||
export class Value<
|
||||
Type extends StaticValidatedAs,
|
||||
StaticValidatedAs = Type,
|
||||
@@ -99,6 +115,7 @@ export class Value<
|
||||
validator,
|
||||
)
|
||||
}
|
||||
/** Like {@link Value.toggle} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicToggle<OuterType = unknown>(
|
||||
a: LazyBuild<
|
||||
{
|
||||
@@ -225,6 +242,7 @@ export class Value<
|
||||
validator,
|
||||
)
|
||||
}
|
||||
/** Like {@link Value.text} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicText<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
@@ -345,6 +363,7 @@ export class Value<
|
||||
return { spec: built, validator }
|
||||
}, validator)
|
||||
}
|
||||
/** Like {@link Value.textarea} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicTextarea<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
@@ -467,6 +486,7 @@ export class Value<
|
||||
validator,
|
||||
)
|
||||
}
|
||||
/** Like {@link Value.number} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicNumber<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
@@ -562,6 +582,7 @@ export class Value<
|
||||
)
|
||||
}
|
||||
|
||||
/** Like {@link Value.color} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicColor<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
@@ -659,6 +680,7 @@ export class Value<
|
||||
validator,
|
||||
)
|
||||
}
|
||||
/** Like {@link Value.datetime} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicDatetime<Required extends boolean, OuterType = unknown>(
|
||||
getA: LazyBuild<
|
||||
{
|
||||
@@ -769,6 +791,7 @@ export class Value<
|
||||
validator,
|
||||
)
|
||||
}
|
||||
/** Like {@link Value.select} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicSelect<
|
||||
Values extends Record<string, string>,
|
||||
OuterType = unknown,
|
||||
@@ -889,6 +912,7 @@ export class Value<
|
||||
validator,
|
||||
)
|
||||
}
|
||||
/** Like {@link Value.multiselect} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicMultiselect<
|
||||
Values extends Record<string, string>,
|
||||
OuterType = unknown,
|
||||
@@ -977,6 +1001,12 @@ export class Value<
|
||||
}
|
||||
}, spec.validator)
|
||||
}
|
||||
/**
|
||||
* Displays a file upload input field.
|
||||
*
|
||||
* @param a.extensions - Allowed file extensions (e.g. `[".pem", ".crt"]`)
|
||||
* @param a.required - Whether a file must be selected
|
||||
*/
|
||||
static file<Required extends boolean>(a: {
|
||||
name: string
|
||||
description?: string | null
|
||||
@@ -1000,6 +1030,7 @@ export class Value<
|
||||
asRequiredParser(fileInfoParser, a),
|
||||
)
|
||||
}
|
||||
/** Like {@link Value.file} but options are resolved lazily at runtime via a builder function. */
|
||||
static dynamicFile<Required extends boolean, OuterType = unknown>(
|
||||
a: LazyBuild<
|
||||
{
|
||||
@@ -1102,6 +1133,7 @@ export class Value<
|
||||
}
|
||||
}, a.variants.validator)
|
||||
}
|
||||
/** Like {@link Value.union} but options (including which variants are available) are resolved lazily at runtime. */
|
||||
static dynamicUnion<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -1123,6 +1155,7 @@ export class Value<
|
||||
OuterType
|
||||
>,
|
||||
): Value<UnionRes<VariantValues>, UnionRes<VariantValues>, OuterType>
|
||||
/** Like {@link Value.union} but options are resolved lazily, with an explicit static validator type. */
|
||||
static dynamicUnion<
|
||||
StaticVariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -1300,6 +1333,12 @@ export class Value<
|
||||
}, z.any())
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the validated output value using a mapping function.
|
||||
* The form field itself remains unchanged, but the value is transformed after validation.
|
||||
*
|
||||
* @param fn - A function to transform the validated value
|
||||
*/
|
||||
map<U>(fn: (value: StaticValidatedAs) => U): Value<U, U, OuterType> {
|
||||
return new Value<U, U, OuterType>(async (options) => {
|
||||
const built = await this.build(options)
|
||||
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
} from './inputSpec'
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* The runtime result type of a discriminated union form field.
|
||||
* Contains `selection` (the chosen variant key), `value` (the variant's form data),
|
||||
* and optionally `other` (partial data from previously selected variants).
|
||||
*/
|
||||
export type UnionRes<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -28,6 +33,7 @@ export type UnionRes<
|
||||
}
|
||||
}[K]
|
||||
|
||||
/** Like {@link UnionRes} but using the static (Zod-inferred) validated types. */
|
||||
export type UnionResStaticValidatedAs<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
@@ -118,6 +124,11 @@ export class Variants<
|
||||
>,
|
||||
) {}
|
||||
readonly _TYPE: UnionRes<VariantValues> = null as any
|
||||
/**
|
||||
* Creates a `Variants` instance from a record mapping variant keys to their display name and form spec.
|
||||
*
|
||||
* @param a - A record of `{ name: string, spec: InputSpec }` entries, one per variant
|
||||
*/
|
||||
static of<
|
||||
VariantValues extends {
|
||||
[K in string]: {
|
||||
|
||||
@@ -5,42 +5,124 @@ import { Value } from './builder/value'
|
||||
import { Variants } from './builder/variants'
|
||||
|
||||
/**
|
||||
* Base SMTP settings, to be used by StartOS for system wide SMTP
|
||||
* Creates an SMTP field spec with provider-specific defaults pre-filled.
|
||||
*/
|
||||
export const customSmtp: InputSpec<SmtpValue> = InputSpec.of<
|
||||
InputSpecOf<SmtpValue>
|
||||
>({
|
||||
server: Value.text({
|
||||
name: 'SMTP Server',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
port: Value.number({
|
||||
name: 'Port',
|
||||
required: true,
|
||||
default: 587,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
integer: true,
|
||||
}),
|
||||
from: Value.text({
|
||||
name: 'From Address',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'Example Name <test@example.com>',
|
||||
inputmode: 'email',
|
||||
patterns: [Patterns.emailWithName],
|
||||
}),
|
||||
login: Value.text({
|
||||
name: 'Login',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
password: Value.text({
|
||||
name: 'Password',
|
||||
required: false,
|
||||
default: null,
|
||||
masked: true,
|
||||
function smtpFields(
|
||||
defaults: {
|
||||
host?: string
|
||||
port?: number
|
||||
security?: 'starttls' | 'tls'
|
||||
} = {},
|
||||
): InputSpec<SmtpValue> {
|
||||
return InputSpec.of<InputSpecOf<SmtpValue>>({
|
||||
host: Value.text({
|
||||
name: 'Host',
|
||||
required: true,
|
||||
default: defaults.host ?? null,
|
||||
placeholder: 'smtp.example.com',
|
||||
}),
|
||||
port: Value.number({
|
||||
name: 'Port',
|
||||
required: true,
|
||||
default: defaults.port ?? 587,
|
||||
min: 1,
|
||||
max: 65535,
|
||||
integer: true,
|
||||
}),
|
||||
security: Value.select({
|
||||
name: 'Connection Security',
|
||||
default: defaults.security ?? 'starttls',
|
||||
values: {
|
||||
starttls: 'STARTTLS',
|
||||
tls: 'TLS',
|
||||
},
|
||||
}),
|
||||
from: Value.text({
|
||||
name: 'From Address',
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'Example Name <test@example.com>',
|
||||
patterns: [Patterns.emailWithName],
|
||||
}),
|
||||
username: Value.text({
|
||||
name: 'Username',
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
password: Value.text({
|
||||
name: 'Password',
|
||||
required: false,
|
||||
default: null,
|
||||
masked: true,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Base SMTP settings with no provider-specific defaults.
|
||||
*/
|
||||
export const customSmtp = smtpFields()
|
||||
|
||||
/**
|
||||
* Provider presets for SMTP configuration.
|
||||
* Each variant has SMTP fields pre-filled with the provider's recommended settings.
|
||||
*/
|
||||
export const smtpProviderVariants = Variants.of({
|
||||
gmail: {
|
||||
name: 'Gmail',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
ses: {
|
||||
name: 'Amazon SES',
|
||||
spec: smtpFields({
|
||||
host: 'email-smtp.us-east-1.amazonaws.com',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
sendgrid: {
|
||||
name: 'SendGrid',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.sendgrid.net',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
mailgun: {
|
||||
name: 'Mailgun',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.mailgun.org',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
protonmail: {
|
||||
name: 'Proton Mail',
|
||||
spec: smtpFields({
|
||||
host: 'smtp.protonmail.ch',
|
||||
port: 587,
|
||||
security: 'starttls',
|
||||
}),
|
||||
},
|
||||
other: {
|
||||
name: 'Other',
|
||||
spec: customSmtp,
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* System SMTP settings with provider presets.
|
||||
* Wraps smtpProviderVariants in a union for use by the system email settings page.
|
||||
*/
|
||||
export const systemSmtpSpec = InputSpec.of({
|
||||
provider: Value.union({
|
||||
name: 'Provider',
|
||||
default: null as any,
|
||||
variants: smtpProviderVariants,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -55,19 +137,24 @@ const smtpVariants = Variants.of({
|
||||
'A custom from address for this service. If not provided, the system from address will be used.',
|
||||
required: false,
|
||||
default: null,
|
||||
placeholder: '<name>test@example.com',
|
||||
inputmode: 'email',
|
||||
patterns: [Patterns.email],
|
||||
placeholder: 'Name <test@example.com>',
|
||||
patterns: [Patterns.emailWithName],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
custom: {
|
||||
name: 'Custom Credentials',
|
||||
spec: customSmtp,
|
||||
spec: InputSpec.of({
|
||||
provider: Value.union({
|
||||
name: 'Provider',
|
||||
default: null as any,
|
||||
variants: smtpProviderVariants,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
})
|
||||
/**
|
||||
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings
|
||||
* For service inputSpec. Gives users 3 options for SMTP: (1) disabled, (2) use system SMTP settings, (3) use custom SMTP settings with provider presets
|
||||
*/
|
||||
export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => {
|
||||
const smtp = await new GetSystemSmtp(effects).once()
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
/**
|
||||
* A record mapping field keys to their {@link ValueSpec} definitions.
|
||||
* This is the root shape of a dynamic form specification — it defines the complete set
|
||||
* of configurable fields for a service or action.
|
||||
*/
|
||||
export type InputSpec = Record<string, ValueSpec>
|
||||
/**
|
||||
* The discriminator for all supported form field types.
|
||||
*/
|
||||
export type ValueType =
|
||||
| 'text'
|
||||
| 'textarea'
|
||||
@@ -13,6 +21,7 @@ export type ValueType =
|
||||
| 'file'
|
||||
| 'union'
|
||||
| 'hidden'
|
||||
/** Union of all concrete form field spec types. Discriminate on the `type` field. */
|
||||
export type ValueSpec = ValueSpecOf<ValueType>
|
||||
/** core spec types. These types provide the metadata for performing validations */
|
||||
// prettier-ignore
|
||||
@@ -32,37 +41,56 @@ export type ValueSpecOf<T extends ValueType> =
|
||||
T extends "hidden" ? ValueSpecHidden :
|
||||
never
|
||||
|
||||
/** Spec for a single-line text input field. */
|
||||
export type ValueSpecText = {
|
||||
/** Display label for the field. */
|
||||
name: string
|
||||
/** Optional help text displayed below the field. */
|
||||
description: string | null
|
||||
/** Optional warning message displayed to the user. */
|
||||
warning: string | null
|
||||
|
||||
type: 'text'
|
||||
/** Regex patterns used to validate the input value. */
|
||||
patterns: Pattern[]
|
||||
/** Minimum character length, or `null` for no minimum. */
|
||||
minLength: number | null
|
||||
/** Maximum character length, or `null` for no maximum. */
|
||||
maxLength: number | null
|
||||
/** Whether the field should obscure input (e.g. for passwords). */
|
||||
masked: boolean
|
||||
|
||||
/** HTML input mode hint for mobile keyboards. */
|
||||
inputmode: 'text' | 'email' | 'tel' | 'url'
|
||||
/** Placeholder text shown when the field is empty. */
|
||||
placeholder: string | null
|
||||
|
||||
/** Whether the field must have a value. */
|
||||
required: boolean
|
||||
/** Default value, which may be a literal string or a {@link RandomString} generation spec. */
|
||||
default: DefaultString | null
|
||||
/** `false` if editable, or a string message explaining why the field is disabled. */
|
||||
disabled: false | string
|
||||
/** If set, provides a "generate" button that fills the field with a random string matching this spec. */
|
||||
generate: null | RandomString
|
||||
/** Whether the field value cannot be changed after initial configuration. */
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a multi-line textarea input field. */
|
||||
export type ValueSpecTextarea = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'textarea'
|
||||
/** Regex patterns used to validate the input value. */
|
||||
patterns: Pattern[]
|
||||
placeholder: string | null
|
||||
minLength: number | null
|
||||
maxLength: number | null
|
||||
/** Minimum number of visible rows. */
|
||||
minRows: number
|
||||
/** Maximum number of visible rows before scrolling. */
|
||||
maxRows: number
|
||||
required: boolean
|
||||
default: string | null
|
||||
@@ -70,12 +98,18 @@ export type ValueSpecTextarea = {
|
||||
immutable: boolean
|
||||
}
|
||||
|
||||
/** Spec for a numeric input field. */
|
||||
export type ValueSpecNumber = {
|
||||
type: 'number'
|
||||
/** Minimum allowed value, or `null` for unbounded. */
|
||||
min: number | null
|
||||
/** Maximum allowed value, or `null` for unbounded. */
|
||||
max: number | null
|
||||
/** Whether only whole numbers are accepted. */
|
||||
integer: boolean
|
||||
/** Step increment for the input spinner, or `null` for any precision. */
|
||||
step: number | null
|
||||
/** Display label for the unit (e.g. `"MB"`, `"seconds"`), shown next to the field. */
|
||||
units: string | null
|
||||
placeholder: string | null
|
||||
name: string
|
||||
@@ -86,6 +120,7 @@ export type ValueSpecNumber = {
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a browser-native color picker field. */
|
||||
export type ValueSpecColor = {
|
||||
name: string
|
||||
description: string | null
|
||||
@@ -93,34 +128,44 @@ export type ValueSpecColor = {
|
||||
|
||||
type: 'color'
|
||||
required: boolean
|
||||
/** Default hex color string (e.g. `"#ff0000"`), or `null`. */
|
||||
default: string | null
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a date, time, or datetime input field. */
|
||||
export type ValueSpecDatetime = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'datetime'
|
||||
required: boolean
|
||||
/** Controls which kind of picker is displayed. */
|
||||
inputmode: 'date' | 'time' | 'datetime-local'
|
||||
/** Minimum selectable date/time as an ISO string, or `null`. */
|
||||
min: string | null
|
||||
/** Maximum selectable date/time as an ISO string, or `null`. */
|
||||
max: string | null
|
||||
default: string | null
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a single-select field displayed as radio buttons in a modal. */
|
||||
export type ValueSpecSelect = {
|
||||
/** Map of option keys to display labels. */
|
||||
values: Record<string, string>
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'select'
|
||||
default: string | null
|
||||
/** `false` if all enabled, a string disabling the whole field, or an array of disabled option keys. */
|
||||
disabled: false | string | string[]
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a multi-select field displayed as checkboxes in a modal. */
|
||||
export type ValueSpecMultiselect = {
|
||||
/** Map of option keys to display labels. */
|
||||
values: Record<string, string>
|
||||
|
||||
name: string
|
||||
@@ -128,12 +173,17 @@ export type ValueSpecMultiselect = {
|
||||
warning: string | null
|
||||
|
||||
type: 'multiselect'
|
||||
/** Minimum number of selections required, or `null`. */
|
||||
minLength: number | null
|
||||
/** Maximum number of selections allowed, or `null`. */
|
||||
maxLength: number | null
|
||||
/** `false` if all enabled, a string disabling the whole field, or an array of disabled option keys. */
|
||||
disabled: false | string | string[]
|
||||
/** Array of option keys selected by default. */
|
||||
default: string[]
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a boolean toggle (on/off switch). */
|
||||
export type ValueSpecToggle = {
|
||||
name: string
|
||||
description: string | null
|
||||
@@ -144,57 +194,81 @@ export type ValueSpecToggle = {
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
}
|
||||
/**
|
||||
* Spec for a discriminated union field — displays a dropdown for variant selection,
|
||||
* and each variant can have its own nested sub-form.
|
||||
*/
|
||||
export type ValueSpecUnion = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'union'
|
||||
/** Map of variant keys to their display name and nested form spec. */
|
||||
variants: Record<
|
||||
string,
|
||||
{
|
||||
/** Display name for this variant in the dropdown. */
|
||||
name: string
|
||||
/** Nested form spec shown when this variant is selected. */
|
||||
spec: InputSpec
|
||||
}
|
||||
>
|
||||
/** `false` if all enabled, a string disabling the whole field, or an array of disabled variant keys. */
|
||||
disabled: false | string | string[]
|
||||
default: string | null
|
||||
immutable: boolean
|
||||
}
|
||||
/** Spec for a file upload input field. */
|
||||
export type ValueSpecFile = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'file'
|
||||
/** Allowed file extensions (e.g. `[".pem", ".crt"]`). */
|
||||
extensions: string[]
|
||||
required: boolean
|
||||
}
|
||||
/** Spec for a collapsible grouping of nested fields (a "sub-form"). */
|
||||
export type ValueSpecObject = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'object'
|
||||
/** The nested form spec containing this object's fields. */
|
||||
spec: InputSpec
|
||||
}
|
||||
/** Spec for a hidden field — not displayed to the user but included in the form data. */
|
||||
export type ValueSpecHidden = {
|
||||
type: 'hidden'
|
||||
}
|
||||
/** The two supported list item types. */
|
||||
export type ListValueSpecType = 'text' | 'object'
|
||||
/** Maps a {@link ListValueSpecType} to its concrete list item spec. */
|
||||
// prettier-ignore
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
T extends "text" ? ListValueSpecText :
|
||||
T extends "object" ? ListValueSpecObject :
|
||||
never
|
||||
/** A list field spec — union of text-list and object-list variants. */
|
||||
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
|
||||
/**
|
||||
* Spec for a list field — an interface to add, remove, and edit items in an ordered collection.
|
||||
* The `spec` field determines whether list items are text strings or structured objects.
|
||||
*/
|
||||
export type ValueSpecListOf<T extends ListValueSpecType> = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'list'
|
||||
/** The item spec — determines whether this is a list of text values or objects. */
|
||||
spec: ListValueSpecOf<T>
|
||||
/** Minimum number of items, or `null` for no minimum. */
|
||||
minLength: number | null
|
||||
/** Maximum number of items, or `null` for no maximum. */
|
||||
maxLength: number | null
|
||||
disabled: false | string
|
||||
/** Default list items to populate on creation. */
|
||||
default:
|
||||
| string[]
|
||||
| DefaultString[]
|
||||
@@ -203,10 +277,14 @@ export type ValueSpecListOf<T extends ListValueSpecType> = {
|
||||
| readonly DefaultString[]
|
||||
| readonly Record<string, unknown>[]
|
||||
}
|
||||
/** A regex validation pattern with a human-readable description of what it enforces. */
|
||||
export type Pattern = {
|
||||
/** The regex pattern string (without delimiters). */
|
||||
regex: string
|
||||
/** A user-facing explanation shown when validation fails (e.g. `"Must be a valid email"`). */
|
||||
description: string
|
||||
}
|
||||
/** Spec for text items within a list field. */
|
||||
export type ListValueSpecText = {
|
||||
type: 'text'
|
||||
patterns: Pattern[]
|
||||
@@ -218,13 +296,24 @@ export type ListValueSpecText = {
|
||||
inputmode: 'text' | 'email' | 'tel' | 'url'
|
||||
placeholder: string | null
|
||||
}
|
||||
/** Spec for object items within a list field. */
|
||||
export type ListValueSpecObject = {
|
||||
type: 'object'
|
||||
/** The form spec for each object item. */
|
||||
spec: InputSpec
|
||||
/** Defines how uniqueness is determined among list items. */
|
||||
uniqueBy: UniqueBy
|
||||
/** An expression used to generate the display string for each item in the list summary (e.g. a key path). */
|
||||
displayAs: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes how list items determine uniqueness.
|
||||
* - `null`: no uniqueness constraint
|
||||
* - `string`: unique by a specific field key
|
||||
* - `{ any: UniqueBy[] }`: unique if any of the sub-constraints match
|
||||
* - `{ all: UniqueBy[] }`: unique if all sub-constraints match together
|
||||
*/
|
||||
export type UniqueBy =
|
||||
| null
|
||||
| string
|
||||
@@ -234,12 +323,21 @@ export type UniqueBy =
|
||||
| {
|
||||
all: readonly UniqueBy[] | UniqueBy[]
|
||||
}
|
||||
/** A default value that is either a literal string or a {@link RandomString} generation spec. */
|
||||
export type DefaultString = string | RandomString
|
||||
/** Spec for generating a random string — used for default passwords, API keys, etc. */
|
||||
export type RandomString = {
|
||||
/** The character set to draw from (e.g. `"a-zA-Z0-9"`). */
|
||||
charset: string
|
||||
/** The length of the generated string. */
|
||||
len: number
|
||||
}
|
||||
// sometimes the type checker needs just a little bit of help
|
||||
/**
|
||||
* Type guard that narrows a {@link ValueSpec} to a {@link ValueSpecListOf} of a specific item type.
|
||||
*
|
||||
* @param t - The value spec to check
|
||||
* @param s - The list item type to narrow to (`"text"` or `"object"`)
|
||||
*/
|
||||
export function isValueSpecListOf<S extends ListValueSpecType>(
|
||||
t: ValueSpec,
|
||||
s: S,
|
||||
|
||||
Reference in New Issue
Block a user