|
@@ -115,6 +115,19 @@ export function NodeConfigModal({
|
|
|
{ busy: false, message: '', error: false }
|
|
{ busy: false, message: '', error: false }
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ // Numeric settings that are being given an expression instead of a number.
|
|
|
|
|
+ // A value that already holds {{ }} is one by definition; this covers the
|
|
|
|
|
+ // moment after switching, when the box is still empty.
|
|
|
|
|
+ const [asExpression, setAsExpression] = useState<Set<string>>(new Set())
|
|
|
|
|
+ const toggleExpression = (key: string, on: boolean) => {
|
|
|
|
|
+ setAsExpression((prev) => {
|
|
|
|
|
+ const next = new Set(prev)
|
|
|
|
|
+ if (on) next.add(key)
|
|
|
|
|
+ else next.delete(key)
|
|
|
|
|
+ return next
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const [importing, setImporting] = useState<{
|
|
const [importing, setImporting] = useState<{
|
|
|
field: string
|
|
field: string
|
|
|
source: string
|
|
source: string
|
|
@@ -578,16 +591,72 @@ export function NodeConfigModal({
|
|
|
<span className="text-sm text-gray-600 dark:text-gray-400">{prop.description}</span>
|
|
<span className="text-sm text-gray-600 dark:text-gray-400">{prop.description}</span>
|
|
|
</label>
|
|
</label>
|
|
|
) : prop.type === 'number' || prop.type === 'integer' ? (
|
|
) : prop.type === 'number' || prop.type === 'integer' ? (
|
|
|
- <input
|
|
|
|
|
- type="number"
|
|
|
|
|
- value={editingConfig[key] ?? prop.default ?? ''}
|
|
|
|
|
- onChange={(e) =>
|
|
|
|
|
- onConfigChange({ ...editingConfig, [key]: prop.type === 'integer' ? parseInt(e.target.value, 10) : Number(e.target.value) })
|
|
|
|
|
- }
|
|
|
|
|
- step={prop.type === 'integer' ? 1 : undefined}
|
|
|
|
|
- placeholder={prop.description}
|
|
|
|
|
- className="w-full px-3 py-2 border border-gray-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ (() => {
|
|
|
|
|
+ const current = editingConfig[key]
|
|
|
|
|
+ // A number box cannot hold {{ }}, so a width that comes
|
|
|
|
|
+ // from an earlier node had nowhere to go. Either form is
|
|
|
|
|
+ // allowed and the field remembers which it is in.
|
|
|
|
|
+ const usingExpression =
|
|
|
|
|
+ asExpression.has(key) ||
|
|
|
|
|
+ (typeof current === 'string' && current.includes('{{'))
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="space-y-1">
|
|
|
|
|
+ {usingExpression ? (
|
|
|
|
|
+ <ExpressionInput
|
|
|
|
|
+ value={String(current ?? '')}
|
|
|
|
|
+ onChange={(value) =>
|
|
|
|
|
+ onConfigChange({ ...editingConfig, [key]: value })
|
|
|
|
|
+ }
|
|
|
|
|
+ placeholder={`An expression giving a number, such as {{data.result.width}}`}
|
|
|
|
|
+ availableFields={getUpstreamOutputFields(selectedNodeData.id).map((f) => ({
|
|
|
|
|
+ path: f.path,
|
|
|
|
|
+ type: f.type,
|
|
|
|
|
+ nodeName: f.nodeName,
|
|
|
|
|
+ nodeId: f.nodeId,
|
|
|
|
|
+ displayName: f.displayName,
|
|
|
|
|
+ }))}
|
|
|
|
|
+ />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ value={current ?? prop.default ?? ''}
|
|
|
|
|
+ onChange={(e) =>
|
|
|
|
|
+ onConfigChange({
|
|
|
|
|
+ ...editingConfig,
|
|
|
|
|
+ // An empty box is not zero. Clearing it has
|
|
|
|
|
+ // to leave the setting unset, or a node that
|
|
|
|
|
+ // falls back to its own default never can.
|
|
|
|
|
+ [key]: e.target.value === ''
|
|
|
|
|
+ ? ''
|
|
|
|
|
+ : prop.type === 'integer'
|
|
|
|
|
+ ? parseInt(e.target.value, 10)
|
|
|
|
|
+ : Number(e.target.value),
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ step={prop.type === 'integer' ? 1 : undefined}
|
|
|
|
|
+ placeholder={prop.description}
|
|
|
|
|
+ className="w-full px-3 py-2 border border-gray-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ const goingToExpression = !usingExpression
|
|
|
|
|
+ toggleExpression(key, goingToExpression)
|
|
|
|
|
+ // Switching back drops an expression rather than
|
|
|
|
|
+ // leaving a string in a numeric setting, where it
|
|
|
|
|
+ // would reach the node as text.
|
|
|
|
|
+ if (!goingToExpression && typeof current === 'string') {
|
|
|
|
|
+ onConfigChange({ ...editingConfig, [key]: '' })
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="text-[11px] text-primary-600 dark:text-primary-400 hover:underline"
|
|
|
|
|
+ >
|
|
|
|
|
+ {usingExpression ? 'Use a number instead' : 'Use a value from another node'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
|
|
+ })()
|
|
|
) : prop.dynamicOptions?.source === 'workflows' ? (
|
|
) : prop.dynamicOptions?.source === 'workflows' ? (
|
|
|
<select
|
|
<select
|
|
|
value={editingConfig[key] ?? ''}
|
|
value={editingConfig[key] ?? ''}
|