Add improved filtering in conditions block of workflows
This commit is contained in:
@@ -33,20 +33,6 @@ export interface WorkflowExecutionWithDetails extends WorkflowExecution {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class WorkflowService {
|
export class WorkflowService {
|
||||||
/**
|
|
||||||
* Check if a workflow has active executions
|
|
||||||
*/
|
|
||||||
private static async hasActiveExecutions(workflowId: string): Promise<number> {
|
|
||||||
return prisma.workflowExecution.count({
|
|
||||||
where: {
|
|
||||||
workflowId,
|
|
||||||
status: {
|
|
||||||
in: [WorkflowExecutionStatus.RUNNING, WorkflowExecutionStatus.WAITING],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all workflows for a project with pagination
|
* Get all workflows for a project with pagination
|
||||||
*/
|
*/
|
||||||
@@ -400,8 +386,7 @@ export class WorkflowService {
|
|||||||
|
|
||||||
if (activeExecutions > 0) {
|
if (activeExecutions > 0) {
|
||||||
// Only allow safe changes: name and position updates
|
// Only allow safe changes: name and position updates
|
||||||
const hasCriticalChanges =
|
const hasCriticalChanges = data.config !== undefined || data.templateId !== undefined;
|
||||||
data.config !== undefined || data.templateId !== undefined;
|
|
||||||
|
|
||||||
if (hasCriticalChanges) {
|
if (hasCriticalChanges) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@@ -930,10 +915,7 @@ export class WorkflowService {
|
|||||||
/**
|
/**
|
||||||
* Cancel all active executions for a workflow
|
* Cancel all active executions for a workflow
|
||||||
*/
|
*/
|
||||||
public static async cancelAllExecutions(
|
public static async cancelAllExecutions(projectId: string, workflowId: string): Promise<{cancelled: number}> {
|
||||||
projectId: string,
|
|
||||||
workflowId: string,
|
|
||||||
): Promise<{cancelled: number}> {
|
|
||||||
// Verify workflow exists and belongs to project
|
// Verify workflow exists and belongs to project
|
||||||
await this.get(projectId, workflowId);
|
await this.get(projectId, workflowId);
|
||||||
|
|
||||||
@@ -959,22 +941,49 @@ export class WorkflowService {
|
|||||||
* Get all available fields for workflow conditions (contact fields + event fields)
|
* Get all available fields for workflow conditions (contact fields + event fields)
|
||||||
*/
|
*/
|
||||||
public static async getAvailableFields(projectId: string, eventName?: string) {
|
public static async getAvailableFields(projectId: string, eventName?: string) {
|
||||||
// Get contact fields (standard + custom data fields)
|
// Get contact fields with types (standard + custom data fields)
|
||||||
const contactFieldsWithTypes = await ContactService.getAvailableFields(projectId);
|
const contactFieldsWithTypes = await ContactService.getAvailableFields(projectId);
|
||||||
|
|
||||||
// Extract just the field names and prefix with 'contact.'
|
// Build typed field list with 'contact.' prefix
|
||||||
const contactFields = contactFieldsWithTypes.map(f => `contact.${f.field}`);
|
const contactFields = contactFieldsWithTypes.map(f => ({
|
||||||
|
field: `contact.${f.field}`,
|
||||||
|
type: f.type,
|
||||||
|
category: f.field.startsWith('data.') ? 'Custom Data' : 'Contact Fields',
|
||||||
|
}));
|
||||||
|
|
||||||
// Get event fields by analyzing actual event data
|
// Get event fields by analyzing actual event data
|
||||||
// This will only show fields that have been seen in actual events
|
// Event fields are treated as dynamic (unknown type at runtime)
|
||||||
const eventFields = await EventService.getAvailableEventFields(projectId, eventName);
|
const eventFieldNames = await EventService.getAvailableEventFields(projectId, eventName);
|
||||||
|
const eventFields = eventFieldNames.map(field => ({
|
||||||
|
field,
|
||||||
|
type: 'string' as const, // Event fields default to string, can contain any JSON value
|
||||||
|
category: 'Event Data',
|
||||||
|
}));
|
||||||
|
|
||||||
// Combine all fields
|
// Combine all fields
|
||||||
const allFields = [...contactFields, ...eventFields].sort();
|
const allFields = [...contactFields, ...eventFields].sort((a, b) => a.field.localeCompare(b.field));
|
||||||
|
|
||||||
|
// Also return legacy format for backwards compatibility
|
||||||
|
const fieldNames = allFields.map(f => f.field);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fields: allFields,
|
fields: fieldNames,
|
||||||
|
typedFields: allFields,
|
||||||
count: allFields.length,
|
count: allFields.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a workflow has active executions
|
||||||
|
*/
|
||||||
|
private static async hasActiveExecutions(workflowId: string): Promise<number> {
|
||||||
|
return prisma.workflowExecution.count({
|
||||||
|
where: {
|
||||||
|
workflowId,
|
||||||
|
status: {
|
||||||
|
in: [WorkflowExecutionStatus.RUNNING, WorkflowExecutionStatus.WAITING],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -828,9 +828,32 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
|||||||
const [conditionField, setConditionField] = useState('');
|
const [conditionField, setConditionField] = useState('');
|
||||||
const [conditionOperator, setConditionOperator] = useState('equals');
|
const [conditionOperator, setConditionOperator] = useState('equals');
|
||||||
const [conditionValue, setConditionValue] = useState('');
|
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);
|
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
|
// WAIT_FOR_EVENT fields
|
||||||
const [eventName, setEventName] = useState('');
|
const [eventName, setEventName] = useState('');
|
||||||
const [eventTimeoutAmount, setEventTimeoutAmount] = useState('1');
|
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
|
// Pass eventName as query param to filter event fields
|
||||||
const url = eventName ? `/workflows/fields?eventName=${encodeURIComponent(eventName)}` : '/workflows/fields';
|
const url = eventName ? `/workflows/fields?eventName=${encodeURIComponent(eventName)}` : '/workflows/fields';
|
||||||
const response = await network.fetch<{fields: string[]}>('GET', url);
|
const response = await network.fetch<{
|
||||||
setAvailableFields(response.fields);
|
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
|
// Set default field if available
|
||||||
if (response.fields.length > 0 && !conditionField) {
|
if (response.typedFields && response.typedFields.length > 0 && !conditionField) {
|
||||||
setConditionField(response.fields[0]!);
|
setConditionField(response.typedFields[0]!.field);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch available fields:', 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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [type, open, workflow]);
|
}, [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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
@@ -1196,15 +1244,32 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
|||||||
</div>
|
</div>
|
||||||
) : availableFields.length > 0 ? (
|
) : availableFields.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Select value={conditionField} onValueChange={setConditionField} required>
|
<Select value={conditionField} onValueChange={handleConditionFieldChange} required>
|
||||||
<SelectTrigger id="conditionField">
|
<SelectTrigger id="conditionField">
|
||||||
<SelectValue placeholder="Select a field..." />
|
<SelectValue placeholder="Select a field..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{availableFields.map(field => (
|
{/* Group fields by category */}
|
||||||
<SelectItem key={field} value={field}>
|
{Object.entries(
|
||||||
{field}
|
availableFields.reduce<Record<string, typeof availableFields>>((acc, field) => {
|
||||||
</SelectItem>
|
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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1221,7 +1286,7 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
|||||||
value={conditionField}
|
value={conditionField}
|
||||||
onChange={e => setConditionField(e.target.value)}
|
onChange={e => setConditionField(e.target.value)}
|
||||||
required
|
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">
|
<p className="text-xs text-neutral-500 mt-1">
|
||||||
No fields found in contacts. Enter a field manually.
|
No fields found in contacts. Enter a field manually.
|
||||||
@@ -1237,34 +1302,63 @@ function AddStepDialog({open, onOpenChange, workflowId, onSuccess}: AddStepDialo
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="equals">Equals</SelectItem>
|
{validOperators.map(op => (
|
||||||
<SelectItem value="notEquals">Not Equals</SelectItem>
|
<SelectItem key={op.value} value={op.value}>
|
||||||
<SelectItem value="contains">Contains</SelectItem>
|
{op.label}
|
||||||
<SelectItem value="notContains">Not Contains</SelectItem>
|
</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>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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>
|
||||||
|
|
||||||
<div>
|
{needsValue && (
|
||||||
<Label htmlFor="conditionValue">Value *</Label>
|
<div>
|
||||||
<Input
|
<Label htmlFor="conditionValue">Value *</Label>
|
||||||
id="conditionValue"
|
{currentFieldType === 'boolean' ? (
|
||||||
type="text"
|
<Select value={conditionValue || 'true'} onValueChange={setConditionValue}>
|
||||||
value={conditionValue}
|
<SelectTrigger id="conditionValue">
|
||||||
onChange={e => setConditionValue(e.target.value)}
|
<SelectValue />
|
||||||
required
|
</SelectTrigger>
|
||||||
placeholder="e.g., true, false, premium, 100"
|
<SelectContent>
|
||||||
/>
|
<SelectItem value="true">True</SelectItem>
|
||||||
<p className="text-xs text-neutral-500 mt-1">
|
<SelectItem value="false">False</SelectItem>
|
||||||
Enter: true/false for booleans, numbers for comparisons, or text for strings
|
</SelectContent>
|
||||||
</p>
|
</Select>
|
||||||
</div>
|
) : 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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1485,9 +1579,52 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
|||||||
});
|
});
|
||||||
const [conditionOperator, setConditionOperator] = useState(String(config?.operator || 'equals'));
|
const [conditionOperator, setConditionOperator] = useState(String(config?.operator || 'equals'));
|
||||||
const [conditionValue, setConditionValue] = useState(String(config?.value ?? ''));
|
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);
|
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
|
// WAIT_FOR_EVENT fields
|
||||||
const [eventName, setEventName] = useState(String(config?.eventName || ''));
|
const [eventName, setEventName] = useState(String(config?.eventName || ''));
|
||||||
const [eventTimeoutAmount, setEventTimeoutAmount] = useState<string>(() => {
|
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
|
// Pass eventName as query param to filter event fields
|
||||||
const url = eventName ? `/workflows/fields?eventName=${encodeURIComponent(eventName)}` : '/workflows/fields';
|
const url = eventName ? `/workflows/fields?eventName=${encodeURIComponent(eventName)}` : '/workflows/fields';
|
||||||
const response = await network.fetch<{fields: string[]}>('GET', url);
|
const response = await network.fetch<{
|
||||||
setAvailableFields(response.fields);
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch available fields:', error);
|
console.error('Failed to fetch available fields:', error);
|
||||||
setAvailableFields([]);
|
setAvailableFields([]);
|
||||||
@@ -1814,15 +1956,32 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
|||||||
</div>
|
</div>
|
||||||
) : availableFields.length > 0 ? (
|
) : availableFields.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Select value={conditionField} onValueChange={setConditionField} required>
|
<Select value={conditionField} onValueChange={handleConditionFieldChange} required>
|
||||||
<SelectTrigger id="editConditionField">
|
<SelectTrigger id="editConditionField">
|
||||||
<SelectValue placeholder="Select a field..." />
|
<SelectValue placeholder="Select a field..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{availableFields.map(field => (
|
{/* Group fields by category */}
|
||||||
<SelectItem key={field} value={field}>
|
{Object.entries(
|
||||||
{field}
|
availableFields.reduce<Record<string, typeof availableFields>>((acc, field) => {
|
||||||
</SelectItem>
|
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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1839,7 +1998,7 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
|||||||
value={conditionField}
|
value={conditionField}
|
||||||
onChange={e => setConditionField(e.target.value)}
|
onChange={e => setConditionField(e.target.value)}
|
||||||
required
|
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">
|
<p className="text-xs text-neutral-500 mt-1">
|
||||||
No fields found in contacts. Enter a field manually.
|
No fields found in contacts. Enter a field manually.
|
||||||
@@ -1855,34 +2014,63 @@ function EditStepDialog({step, workflowId, open, onOpenChange, onSuccess}: EditS
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="equals">Equals</SelectItem>
|
{validOperators.map(op => (
|
||||||
<SelectItem value="notEquals">Not Equals</SelectItem>
|
<SelectItem key={op.value} value={op.value}>
|
||||||
<SelectItem value="contains">Contains</SelectItem>
|
{op.label}
|
||||||
<SelectItem value="notContains">Not Contains</SelectItem>
|
</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>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</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>
|
||||||
|
|
||||||
<div>
|
{needsValue && (
|
||||||
<Label htmlFor="editConditionValue">Value *</Label>
|
<div>
|
||||||
<Input
|
<Label htmlFor="editConditionValue">Value *</Label>
|
||||||
id="editConditionValue"
|
{currentFieldType === 'boolean' ? (
|
||||||
type="text"
|
<Select value={conditionValue || 'true'} onValueChange={setConditionValue}>
|
||||||
value={conditionValue}
|
<SelectTrigger id="editConditionValue">
|
||||||
onChange={e => setConditionValue(e.target.value)}
|
<SelectValue />
|
||||||
required
|
</SelectTrigger>
|
||||||
placeholder="e.g., true, false, premium, 100"
|
<SelectContent>
|
||||||
/>
|
<SelectItem value="true">True</SelectItem>
|
||||||
<p className="text-xs text-neutral-500 mt-1">
|
<SelectItem value="false">False</SelectItem>
|
||||||
Enter: true/false for booleans, numbers for comparisons, or text for strings
|
</SelectContent>
|
||||||
</p>
|
</Select>
|
||||||
</div>
|
) : 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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user