Conditional Rules in Registration Builder Fix
1. Trigger Field Selection - Dropdown to select which field triggers the rule (filters to checkbox, dropdown, radio, text fields) 2. Operator Selection - Dropdown with options: - equals - not equals - contains - is not empty - is empty 3. Value Input - Smart input based on field type: - Checkbox fields → dropdown with "Checked (true)" / "Unchecked (false)" - empty/not_empty operators → disabled (no value needed) - Other fields → text input 4. Action Selection - Dropdown with options: - Show fields - Hide fields - Make required - Make optional 5. Target Fields - Checkbox list of all fields (excluding the trigger field) to select which fields are affected 6. Rule Summary - A blue info box at the bottom of each rule showing a human-readable summary of the configured rule
This commit is contained in:
@@ -1026,45 +1026,213 @@ const AdminRegistrationBuilder = () => {
|
||||
|
||||
{/* Conditional Rules Dialog */}
|
||||
<Dialog open={conditionalDialogOpen} onOpenChange={setConditionalDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Conditional Rules</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4 max-h-96 overflow-y-auto">
|
||||
{(schema?.conditional_rules || []).map((rule, index) => (
|
||||
<div key={rule.id} className="p-4 border rounded-lg space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium">Rule {index + 1}</span>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 text-red-500"
|
||||
onClick={() => {
|
||||
updateSchema((prev) => ({
|
||||
...prev,
|
||||
conditional_rules: prev.conditional_rules.filter((r) => r.id !== rule.id),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Rules allow you to show or hide fields based on other field values. For example, show a "Scholarship Reason" field only when "Request Scholarship" is checked.
|
||||
</p>
|
||||
|
||||
{(schema?.conditional_rules || []).map((rule, index) => {
|
||||
// Get all available fields for dropdowns
|
||||
const allFields = schema?.steps?.flatMap(step =>
|
||||
step.sections?.flatMap(section =>
|
||||
section.fields?.map(field => ({
|
||||
id: field.id,
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
stepTitle: step.title,
|
||||
sectionTitle: section.title,
|
||||
})) || []
|
||||
) || []
|
||||
) || [];
|
||||
|
||||
// Filter trigger fields (checkbox, dropdown, radio work best)
|
||||
const triggerFields = allFields.filter(f =>
|
||||
['checkbox', 'dropdown', 'radio', 'text'].includes(f.type)
|
||||
);
|
||||
|
||||
// Update a specific rule
|
||||
const updateRule = (ruleId, updates) => {
|
||||
updateSchema((prev) => ({
|
||||
...prev,
|
||||
conditional_rules: prev.conditional_rules.map((r) =>
|
||||
r.id === ruleId ? { ...r, ...updates } : r
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
// Get the trigger field's type to determine value input
|
||||
const triggerFieldData = allFields.find(f => f.id === rule.trigger_field);
|
||||
|
||||
return (
|
||||
<div key={rule.id} className="p-4 border rounded-lg space-y-4 bg-gray-50">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-medium text-sm">Rule {index + 1}</span>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 text-red-500 hover:text-red-700"
|
||||
onClick={() => {
|
||||
updateSchema((prev) => ({
|
||||
...prev,
|
||||
conditional_rules: prev.conditional_rules.filter((r) => r.id !== rule.id),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Trigger Field Selection */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">When this field...</Label>
|
||||
<Select
|
||||
value={rule.trigger_field || ''}
|
||||
onValueChange={(value) => updateRule(rule.id, { trigger_field: value })}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue placeholder="Select field" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{triggerFields.map((field) => (
|
||||
<SelectItem key={field.id} value={field.id}>
|
||||
{field.label}
|
||||
<span className="text-xs text-muted-foreground ml-1">
|
||||
({field.type})
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">Operator</Label>
|
||||
<Select
|
||||
value={rule.trigger_operator || 'equals'}
|
||||
onValueChange={(value) => updateRule(rule.id, { trigger_operator: value })}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="equals">equals</SelectItem>
|
||||
<SelectItem value="not_equals">not equals</SelectItem>
|
||||
<SelectItem value="contains">contains</SelectItem>
|
||||
<SelectItem value="not_empty">is not empty</SelectItem>
|
||||
<SelectItem value="empty">is empty</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">Value</Label>
|
||||
{triggerFieldData?.type === 'checkbox' ? (
|
||||
<Select
|
||||
value={String(rule.trigger_value)}
|
||||
onValueChange={(value) =>
|
||||
updateRule(rule.id, { trigger_value: value === 'true' })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">Checked (true)</SelectItem>
|
||||
<SelectItem value="false">Unchecked (false)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : ['empty', 'not_empty'].includes(rule.trigger_operator) ? (
|
||||
<Input
|
||||
className="mt-1"
|
||||
value="(no value needed)"
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
className="mt-1"
|
||||
value={rule.trigger_value || ''}
|
||||
onChange={(e) => updateRule(rule.id, { trigger_value: e.target.value })}
|
||||
placeholder="Enter value"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Selection */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs">Action</Label>
|
||||
<Select
|
||||
value={rule.action || 'show'}
|
||||
onValueChange={(value) => updateRule(rule.id, { action: value })}
|
||||
>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="show">Show fields</SelectItem>
|
||||
<SelectItem value="hide">Hide fields</SelectItem>
|
||||
<SelectItem value="require">Make required</SelectItem>
|
||||
<SelectItem value="optional">Make optional</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">Target Fields</Label>
|
||||
<div className="mt-1 border rounded-md p-2 bg-white max-h-32 overflow-y-auto">
|
||||
{allFields.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No fields available</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{allFields
|
||||
.filter(f => f.id !== rule.trigger_field) // Don't show trigger field as target
|
||||
.map((field) => (
|
||||
<div key={field.id} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={`${rule.id}-${field.id}`}
|
||||
checked={(rule.target_fields || []).includes(field.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
const currentTargets = rule.target_fields || [];
|
||||
const newTargets = checked
|
||||
? [...currentTargets, field.id]
|
||||
: currentTargets.filter((id) => id !== field.id);
|
||||
updateRule(rule.id, { target_fields: newTargets });
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`${rule.id}-${field.id}`}
|
||||
className="text-xs font-normal cursor-pointer"
|
||||
>
|
||||
{field.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rule Summary */}
|
||||
{rule.trigger_field && (rule.target_fields?.length || 0) > 0 && (
|
||||
<div className="text-xs bg-blue-50 border border-blue-200 rounded p-2 text-blue-800">
|
||||
<strong>Summary:</strong> When "{triggerFieldData?.label || rule.trigger_field}" {rule.trigger_operator} "{String(rule.trigger_value)}", {rule.action} the following fields: {rule.target_fields?.map(id => allFields.find(f => f.id === id)?.label || id).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
When <span className="font-mono bg-gray-100 px-1">{rule.trigger_field}</span>{' '}
|
||||
{rule.trigger_operator}{' '}
|
||||
<span className="font-mono bg-gray-100 px-1">{String(rule.trigger_value)}</span>,{' '}
|
||||
{rule.action} fields:{' '}
|
||||
<span className="font-mono bg-gray-100 px-1">
|
||||
{rule.target_fields?.join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{(schema?.conditional_rules || []).length === 0 && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No conditional rules configured. Rules allow you to show or hide fields based on
|
||||
other field values.
|
||||
<div className="text-center py-8 text-muted-foreground border-2 border-dashed rounded-lg">
|
||||
No conditional rules configured yet.<br />
|
||||
Click "Add Rule" to create your first rule.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user