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:
2026-02-02 17:29:03 +07:00
parent b3e6cfba84
commit 82ef36b439
2 changed files with 201 additions and 34 deletions

View File

@@ -7,7 +7,6 @@ const settingsItems = [
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield }, { label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
{ label: 'Theme', path: '/admin/settings/theme', icon: Palette }, { label: 'Theme', path: '/admin/settings/theme', icon: Palette },
{ label: 'Directory', path: '/admin/settings/directory', icon: BookUser }, { label: 'Directory', path: '/admin/settings/directory', icon: BookUser },
// { label: 'Registration', path: '/admin/settings/registration', icon: FileEdit }, // Commented out for future fallback
]; ];
const SettingsTabs = () => { const SettingsTabs = () => {

View File

@@ -1026,19 +1026,55 @@ const AdminRegistrationBuilder = () => {
{/* Conditional Rules Dialog */} {/* Conditional Rules Dialog */}
<Dialog open={conditionalDialogOpen} onOpenChange={setConditionalDialogOpen}> <Dialog open={conditionalDialogOpen} onOpenChange={setConditionalDialogOpen}>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Conditional Rules</DialogTitle> <DialogTitle>Conditional Rules</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4 max-h-96 overflow-y-auto"> <div className="space-y-4 py-4 max-h-[60vh] overflow-y-auto">
{(schema?.conditional_rules || []).map((rule, index) => ( <p className="text-sm text-muted-foreground">
<div key={rule.id} className="p-4 border rounded-lg space-y-3"> 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"> <div className="flex justify-between items-center">
<span className="font-medium">Rule {index + 1}</span> <span className="font-medium text-sm">Rule {index + 1}</span>
<Button <Button
size="icon" size="icon"
variant="ghost" variant="ghost"
className="h-6 w-6 text-red-500" className="h-6 w-6 text-red-500 hover:text-red-700"
onClick={() => { onClick={() => {
updateSchema((prev) => ({ updateSchema((prev) => ({
...prev, ...prev,
@@ -1049,22 +1085,154 @@ const AdminRegistrationBuilder = () => {
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
<div className="text-sm text-muted-foreground">
When <span className="font-mono bg-gray-100 px-1">{rule.trigger_field}</span>{' '} {/* Trigger Field Selection */}
{rule.trigger_operator}{' '} <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<span className="font-mono bg-gray-100 px-1">{String(rule.trigger_value)}</span>,{' '} <div>
{rule.action} fields:{' '} <Label className="text-xs">When this field...</Label>
<span className="font-mono bg-gray-100 px-1"> <Select
{rule.target_fields?.join(', ')} 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> </span>
</SelectItem>
))}
</SelectContent>
</Select>
</div> </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>
</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>
);
})}
{(schema?.conditional_rules || []).length === 0 && ( {(schema?.conditional_rules || []).length === 0 && (
<div className="text-center py-8 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground border-2 border-dashed rounded-lg">
No conditional rules configured. Rules allow you to show or hide fields based on No conditional rules configured yet.<br />
other field values. Click "Add Rule" to create your first rule.
</div> </div>
)} )}