Add improved filtering in conditions block of workflows

This commit is contained in:
Dries Augustyns
2025-12-07 17:35:44 +01:00
parent 4d7c68db3a
commit 82e846e993
2 changed files with 292 additions and 95 deletions
+256 -68
View File
@@ -828,9 +828,32 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
const [conditionField, setConditionField] = useState('');
const [conditionOperator, setConditionOperator] = useState('equals');
const [conditionValue, setConditionValue] = useState('');
const [availableFields, setAvailableFields] = useState<string[]>([]);
const [availableFields, setAvailableFields] = useState<Array<{field: string; type: string; category: string}>>([]);
const [loadingFields, setLoadingFields] = useState(false);
// Get current field type for smart operator filtering
const currentFieldType = availableFields.find(f => f.field === conditionField)?.type || 'string';
// Get valid operators based on field type
const getOperatorsForType = (fieldType: string) => {
const allOperators = [
{value: 'equals', label: 'Equals', types: ['string', 'number', 'boolean', 'date']},
{value: 'notEquals', label: 'Not Equals', types: ['string', 'number', 'boolean', 'date']},
{value: 'contains', label: 'Contains', types: ['string']},
{value: 'notContains', label: 'Does not contain', types: ['string']},
{value: 'greaterThan', label: 'Greater than', types: ['number', 'date']},
{value: 'lessThan', label: 'Less than', types: ['number', 'date']},
{value: 'greaterThanOrEqual', label: 'Greater than or equal', types: ['number', 'date']},
{value: 'lessThanOrEqual', label: 'Less than or equal', types: ['number', 'date']},
{value: 'exists', label: 'Exists', types: ['string', 'number', 'boolean', 'date']},
{value: 'notExists', label: 'Does not exist', types: ['string', 'number', 'boolean', 'date']},
];
return allOperators.filter(op => op.types.includes(fieldType));
};
const validOperators = getOperatorsForType(currentFieldType);
const needsValue = !['exists', 'notExists'].includes(conditionOperator);
// WAIT_FOR_EVENT fields
const [eventName, setEventName] = useState('');
const [eventTimeoutAmount, setEventTimeoutAmount] = useState('1');
@@ -869,12 +892,17 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
// Pass eventName as query param to filter event fields
const url = eventName ? `/workflows/fields?eventName=${encodeURIComponent(eventName)}` : '/workflows/fields';
const response = await network.fetch<{fields: string[]}>('GET', url);
setAvailableFields(response.fields);
const response = await network.fetch<{
fields: string[];
typedFields: Array<{field: string; type: string; category: string}>;
}>('GET', url);
setAvailableFields(
response.typedFields || response.fields.map(f => ({field: f, type: 'string', category: 'Unknown'})),
);
// Set default field if available
if (response.fields.length > 0 && !conditionField) {
setConditionField(response.fields[0]!);
if (response.typedFields && response.typedFields.length > 0 && !conditionField) {
setConditionField(response.typedFields[0]!.field);
}
} catch (error) {
console.error('Failed to fetch available fields:', error);
@@ -889,6 +917,26 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type, open, workflow]);
// Handle condition field change - reset operator if not valid for new type
const handleConditionFieldChange = (newField: string) => {
const newFieldType = availableFields.find(f => f.field === newField)?.type || 'string';
const newValidOperators = getOperatorsForType(newFieldType);
setConditionField(newField);
// Reset operator if current one is not valid for new field type
if (!newValidOperators.some(op => op.value === conditionOperator)) {
setConditionOperator('equals');
}
// Reset value when switching to boolean
if (newFieldType === 'boolean') {
setConditionValue('true');
} else if (currentFieldType === 'boolean' && newFieldType !== 'boolean') {
setConditionValue('');
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
@@ -1196,15 +1244,32 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
</div>
) : availableFields.length > 0 ? (
<>
<Select value={conditionField} onValueChange={setConditionField} required>
<Select value={conditionField} onValueChange={handleConditionFieldChange} required>
<SelectTrigger id="conditionField">
<SelectValue placeholder="Select a field..." />
</SelectTrigger>
<SelectContent>
{availableFields.map(field => (
<SelectItem key={field} value={field}>
{field}
</SelectItem>
{/* Group fields by category */}
{Object.entries(
availableFields.reduce<Record<string, typeof availableFields>>((acc, field) => {
if (!acc[field.category]) acc[field.category] = [];
acc[field.category]!.push(field);
return acc;
}, {}),
).map(([category, fields]) => (
<div key={category}>
<div className="px-2 py-1.5 text-xs font-semibold text-neutral-500">{category}</div>
{fields.map(field => (
<SelectItem key={field.field} value={field.field}>
<div className="flex items-center gap-2">
<span>{field.field.replace('contact.', '').replace('data.', '')}</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-neutral-200 text-neutral-600 font-mono">
{field.type}
</span>
</div>
</SelectItem>
))}
</div>
))}
</SelectContent>
</Select>
@@ -1221,7 +1286,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
value={conditionField}
onChange={e => setConditionField(e.target.value)}
required
placeholder="e.g., subscribed or data.plan"
placeholder="e.g., contact.subscribed or contact.data.plan"
/>
<p className="text-xs text-neutral-500 mt-1">
No fields found in contacts. Enter a field manually.
@@ -1237,34 +1302,63 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="equals">Equals</SelectItem>
<SelectItem value="notEquals">Not Equals</SelectItem>
<SelectItem value="contains">Contains</SelectItem>
<SelectItem value="notContains">Not Contains</SelectItem>
<SelectItem value="greaterThan">Greater Than</SelectItem>
<SelectItem value="lessThan">Less Than</SelectItem>
<SelectItem value="greaterThanOrEqual">Greater Than or Equal</SelectItem>
<SelectItem value="lessThanOrEqual">Less Than or Equal</SelectItem>
<SelectItem value="exists">Exists</SelectItem>
<SelectItem value="notExists">Not Exists</SelectItem>
{validOperators.map(op => (
<SelectItem key={op.value} value={op.value}>
{op.label}
</SelectItem>
))}
</SelectContent>
</Select>
{currentFieldType && (
<p className="text-xs text-neutral-500 mt-1">
Showing operators for{' '}
<span className="font-mono bg-neutral-200 px-1 rounded">{currentFieldType}</span> fields
</p>
)}
</div>
<div>
<Label htmlFor="conditionValue">Value *</Label>
<Input
id="conditionValue"
type="text"
value={conditionValue}
onChange={e => setConditionValue(e.target.value)}
required
placeholder="e.g., true, false, premium, 100"
/>
<p className="text-xs text-neutral-500 mt-1">
Enter: true/false for booleans, numbers for comparisons, or text for strings
</p>
</div>
{needsValue && (
<div>
<Label htmlFor="conditionValue">Value *</Label>
{currentFieldType === 'boolean' ? (
<Select value={conditionValue || 'true'} onValueChange={setConditionValue}>
<SelectTrigger id="conditionValue">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
) : currentFieldType === 'number' ? (
<Input
id="conditionValue"
type="number"
value={conditionValue}
onChange={e => setConditionValue(e.target.value)}
required
placeholder="e.g., 100"
/>
) : currentFieldType === 'date' ? (
<Input
id="conditionValue"
type="datetime-local"
value={conditionValue}
onChange={e => setConditionValue(e.target.value)}
required
/>
) : (
<Input
id="conditionValue"
type="text"
value={conditionValue}
onChange={e => setConditionValue(e.target.value)}
required
placeholder="e.g., premium, active"
/>
)}
</div>
)}
</div>
)}
@@ -1485,9 +1579,52 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
});
const [conditionOperator, setConditionOperator] = useState(String(config?.operator || 'equals'));
const [conditionValue, setConditionValue] = useState(String(config?.value ?? ''));
const [availableFields, setAvailableFields] = useState<string[]>([]);
const [availableFields, setAvailableFields] = useState<Array<{field: string; type: string; category: string}>>([]);
const [loadingFields, setLoadingFields] = useState(false);
// Get current field type for smart operator filtering
const currentFieldType = availableFields.find(f => f.field === conditionField)?.type || 'string';
// Get valid operators based on field type
const getOperatorsForType = (fieldType: string) => {
const allOperators = [
{value: 'equals', label: 'Equals', types: ['string', 'number', 'boolean', 'date']},
{value: 'notEquals', label: 'Not Equals', types: ['string', 'number', 'boolean', 'date']},
{value: 'contains', label: 'Contains', types: ['string']},
{value: 'notContains', label: 'Does not contain', types: ['string']},
{value: 'greaterThan', label: 'Greater than', types: ['number', 'date']},
{value: 'lessThan', label: 'Less than', types: ['number', 'date']},
{value: 'greaterThanOrEqual', label: 'Greater than or equal', types: ['number', 'date']},
{value: 'lessThanOrEqual', label: 'Less than or equal', types: ['number', 'date']},
{value: 'exists', label: 'Exists', types: ['string', 'number', 'boolean', 'date']},
{value: 'notExists', label: 'Does not exist', types: ['string', 'number', 'boolean', 'date']},
];
return allOperators.filter(op => op.types.includes(fieldType));
};
const validOperators = getOperatorsForType(currentFieldType);
const needsValue = !['exists', 'notExists'].includes(conditionOperator);
// Handle condition field change - reset operator if not valid for new type
const handleConditionFieldChange = (newField: string) => {
const newFieldType = availableFields.find(f => f.field === newField)?.type || 'string';
const newValidOperators = getOperatorsForType(newFieldType);
setConditionField(newField);
// Reset operator if current one is not valid for new field type
if (!newValidOperators.some(op => op.value === conditionOperator)) {
setConditionOperator('equals');
}
// Reset value when switching to boolean
if (newFieldType === 'boolean') {
setConditionValue('true');
} else if (currentFieldType === 'boolean' && newFieldType !== 'boolean') {
setConditionValue('');
}
};
// WAIT_FOR_EVENT fields
const [eventName, setEventName] = useState(String(config?.eventName || ''));
const [eventTimeoutAmount, setEventTimeoutAmount] = useState<string>(() => {
@@ -1545,8 +1682,13 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
// Pass eventName as query param to filter event fields
const url = eventName ? `/workflows/fields?eventName=${encodeURIComponent(eventName)}` : '/workflows/fields';
const response = await network.fetch<{fields: string[]}>('GET', url);
setAvailableFields(response.fields);
const response = await network.fetch<{
fields: string[];
typedFields: Array<{field: string; type: string; category: string}>;
}>('GET', url);
setAvailableFields(
response.typedFields || response.fields.map(f => ({field: f, type: 'string', category: 'Unknown'})),
);
} catch (error) {
console.error('Failed to fetch available fields:', error);
setAvailableFields([]);
@@ -1814,15 +1956,32 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
</div>
) : availableFields.length > 0 ? (
<>
<Select value={conditionField} onValueChange={setConditionField} required>
<Select value={conditionField} onValueChange={handleConditionFieldChange} required>
<SelectTrigger id="editConditionField">
<SelectValue placeholder="Select a field..." />
</SelectTrigger>
<SelectContent>
{availableFields.map(field => (
<SelectItem key={field} value={field}>
{field}
</SelectItem>
{/* Group fields by category */}
{Object.entries(
availableFields.reduce<Record<string, typeof availableFields>>((acc, field) => {
if (!acc[field.category]) acc[field.category] = [];
acc[field.category]!.push(field);
return acc;
}, {}),
).map(([category, fields]) => (
<div key={category}>
<div className="px-2 py-1.5 text-xs font-semibold text-neutral-500">{category}</div>
{fields.map(field => (
<SelectItem key={field.field} value={field.field}>
<div className="flex items-center gap-2">
<span>{field.field.replace('contact.', '').replace('data.', '')}</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-neutral-200 text-neutral-600 font-mono">
{field.type}
</span>
</div>
</SelectItem>
))}
</div>
))}
</SelectContent>
</Select>
@@ -1839,7 +1998,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
value={conditionField}
onChange={e => setConditionField(e.target.value)}
required
placeholder="e.g., subscribed or data.plan"
placeholder="e.g., contact.subscribed or contact.data.plan"
/>
<p className="text-xs text-neutral-500 mt-1">
No fields found in contacts. Enter a field manually.
@@ -1855,34 +2014,63 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="equals">Equals</SelectItem>
<SelectItem value="notEquals">Not Equals</SelectItem>
<SelectItem value="contains">Contains</SelectItem>
<SelectItem value="notContains">Not Contains</SelectItem>
<SelectItem value="greaterThan">Greater Than</SelectItem>
<SelectItem value="lessThan">Less Than</SelectItem>
<SelectItem value="greaterThanOrEqual">Greater Than or Equal</SelectItem>
<SelectItem value="lessThanOrEqual">Less Than or Equal</SelectItem>
<SelectItem value="exists">Exists</SelectItem>
<SelectItem value="notExists">Not Exists</SelectItem>
{validOperators.map(op => (
<SelectItem key={op.value} value={op.value}>
{op.label}
</SelectItem>
))}
</SelectContent>
</Select>
{currentFieldType && (
<p className="text-xs text-neutral-500 mt-1">
Showing operators for{' '}
<span className="font-mono bg-neutral-200 px-1 rounded">{currentFieldType}</span> fields
</p>
)}
</div>
<div>
<Label htmlFor="editConditionValue">Value *</Label>
<Input
id="editConditionValue"
type="text"
value={conditionValue}
onChange={e => setConditionValue(e.target.value)}
required
placeholder="e.g., true, false, premium, 100"
/>
<p className="text-xs text-neutral-500 mt-1">
Enter: true/false for booleans, numbers for comparisons, or text for strings
</p>
</div>
{needsValue && (
<div>
<Label htmlFor="editConditionValue">Value *</Label>
{currentFieldType === 'boolean' ? (
<Select value={conditionValue || 'true'} onValueChange={setConditionValue}>
<SelectTrigger id="editConditionValue">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
) : currentFieldType === 'number' ? (
<Input
id="editConditionValue"
type="number"
value={conditionValue}
onChange={e => setConditionValue(e.target.value)}
required
placeholder="e.g., 100"
/>
) : currentFieldType === 'date' ? (
<Input
id="editConditionValue"
type="datetime-local"
value={conditionValue}
onChange={e => setConditionValue(e.target.value)}
required
/>
) : (
<Input
id="editConditionValue"
type="text"
value={conditionValue}
onChange={e => setConditionValue(e.target.value)}
required
placeholder="e.g., premium, active"
/>
)}
</div>
)}
</div>
)}