38 lines
3.4 KiB
TypeScript
38 lines
3.4 KiB
TypeScript
import React, { useState } from 'react';
|
|
import type { Variable, VariableType } from '../../types/project';
|
|
import { formatVariableDefault, parseVariableDefault, validateVariableName } from './configurationUtils';
|
|
import styles from './ActionInspector.module.css';
|
|
|
|
type Props = {
|
|
name: string;
|
|
variable: Variable;
|
|
variables: Record<string, Variable>;
|
|
isNew?: boolean;
|
|
onSave: (name: string, variable: Variable) => void;
|
|
onCancel: () => void;
|
|
};
|
|
|
|
const TYPES: VariableType[] = ['string', 'number', 'boolean', 'object', 'array'];
|
|
|
|
export default function VariableEditor({ name, variable, variables, isNew = false, onSave, onCancel }: Props): React.ReactElement {
|
|
const [nameDraft, setNameDraft] = useState(name);
|
|
const [type, setType] = useState(variable.type);
|
|
const [defaultDraft, setDefaultDraft] = useState(() => formatVariableDefault(variable));
|
|
const [description, setDescription] = useState(variable.description ?? '');
|
|
const nameError = validateVariableName(nameDraft, variables, isNew ? undefined : name);
|
|
let defaultValue: unknown;
|
|
let defaultError: string | null = null;
|
|
try { defaultValue = parseVariableDefault(type, defaultDraft); } catch (error) { defaultError = error instanceof Error ? error.message : String(error); }
|
|
const valid = !nameError && !defaultError;
|
|
|
|
return (
|
|
<div className={styles.configurationEditor} data-testid={`variable-editor-${name || 'new'}`}>
|
|
<label className={styles.formField}><span className={styles.formLabel}>Variable name</span><input className={styles.formInput} aria-label="Variable name" value={nameDraft} onChange={(event) => setNameDraft(event.target.value)} />{nameError && <span className={styles.inlineError} role="alert">{nameError}</span>}</label>
|
|
<label className={styles.formField}><span className={styles.formLabel}>Type</span><select className={styles.formInput} aria-label="Variable type" value={type} onChange={(event) => setType(event.target.value as VariableType)}>{TYPES.map((candidate) => <option key={candidate}>{candidate}</option>)}</select></label>
|
|
<label className={styles.formField}><span className={styles.formLabel}>Default value</span>{type === 'boolean' ? <select className={styles.formInput} aria-label="Variable default value" value={defaultDraft} onChange={(event) => setDefaultDraft(event.target.value)}><option value="">No default</option><option value="true">true</option><option value="false">false</option></select> : <textarea className={styles.formInput} aria-label="Variable default value" value={defaultDraft} onChange={(event) => setDefaultDraft(event.target.value)} placeholder="Optional design-time default" />}{defaultError && <span className={styles.inlineError} role="alert">{defaultError}</span>}<span className={styles.formHint}>Preview copies this value into ephemeral runtime state.</span></label>
|
|
<label className={styles.formField}><span className={styles.formLabel}>Description</span><input className={styles.formInput} aria-label="Variable description" value={description} onChange={(event) => setDescription(event.target.value)} /></label>
|
|
<div className={styles.actionControls}><button type="button" className={styles.primaryButton} disabled={!valid} onClick={() => onSave(nameDraft, { type, ...(defaultValue !== undefined ? { defaultValue } : {}), ...(description ? { description } : {}) })}>Save variable</button><button type="button" className={styles.smallButton} onClick={onCancel}>Cancel</button></div>
|
|
</div>
|
|
);
|
|
}
|