Fix: Reimplement condition builder in dynamic groups to resolve rendering issues.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/460c318b-eb41-42f9-ba67-d0567bd1faa8.jpg
This commit is contained in:
alphaeusmote
2025-04-10 12:42:12 +00:00
parent 6a939c13a8
commit 2a90e90a6a
@@ -6,22 +6,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { import { Trash, Plus, Move, FolderPlus, ChevronDown, ChevronRight } from 'lucide-react';
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable
} from '@dnd-kit/sortable';
import { Trash, Plus, Move, PlusCircle, FolderPlus, ChevronDown, ChevronRight } from 'lucide-react';
import { Condition } from './rule-editor'; import { Condition } from './rule-editor';
import { LdapAttribute } from '@shared/schema'; import { LdapAttribute } from '@shared/schema';
@@ -61,6 +46,7 @@ const defaultGroup: Condition = {
isGroup: true isGroup: true
}; };
// This is a completely new implementation of the condition builder to avoid recursive rendering
const ConditionBuilder: React.FC<ConditionBuilderProps> = ({ const ConditionBuilder: React.FC<ConditionBuilderProps> = ({
conditions, conditions,
onChange, onChange,
@@ -186,42 +172,6 @@ const ConditionBuilder: React.FC<ConditionBuilderProps> = ({
} }
}; };
// Setup drag sensors
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
// Handle drag and drop
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
if (active.id !== over.id) {
const activeIndex = conditions.findIndex(c => c.id === active.id);
const overIndex = conditions.findIndex(c => c.id === over.id);
// Don't allow dropping a condition outside its parent group
const sourceCondition = conditions[activeIndex];
const overCondition = conditions[overIndex];
if (sourceCondition.parentId !== overCondition.parentId) {
toast({
title: "Invalid Move",
description: "Conditions can only be reordered within the same group",
variant: "destructive"
});
return;
}
const newConditions = arrayMove(conditions, activeIndex, overIndex);
onChange(newConditions);
}
};
// Toggle group expansion // Toggle group expansion
const toggleGroupExpand = (groupId: number) => { const toggleGroupExpand = (groupId: number) => {
setExpandedGroups(prev => { setExpandedGroups(prev => {
@@ -244,182 +194,162 @@ const ConditionBuilder: React.FC<ConditionBuilderProps> = ({
); );
}; };
// Removed unused renderConditionGroup function // Render a single condition
const renderCondition = (condition: Condition, index: number, level = 0) => {
// Sortable Item wrapper component if (condition.isGroup) {
const SortableItem = ({ // Render condition group
condition, return (
index, <div
level = 0 key={`group-${index}`}
}: { className="border border-border rounded-md p-3 mb-3"
condition: Condition; style={{ marginLeft: level > 0 ? `${level * 20}px` : '0' }}
index: number; >
level?: number; <div className="flex items-center justify-between mb-2">
}) => { <div className="flex items-center space-x-2">
const { <button
attributes, type="button"
listeners, onClick={() => condition.id && toggleGroupExpand(condition.id)}
setNodeRef, className="p-1 hover:bg-accent rounded-sm"
transform, >
transition, {condition.id && expandedGroups.has(condition.id) ? (
isDragging <ChevronDown className="h-4 w-4" />
} = useSortable({ ) : (
id: condition.id || index, <ChevronRight className="h-4 w-4" />
data: { )}
condition, </button>
index,
level,
type: condition.isGroup ? 'group' : 'condition'
}
});
const style = {
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
transition,
marginLeft: level > 0 ? `${level * 20}px` : '0',
zIndex: isDragging ? 100 : 1,
position: 'relative' as 'relative',
opacity: isDragging ? 0.5 : 1,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
>
{condition.isGroup ? (
<div
key={`group-${index}`}
className="border border-border rounded-md p-3 mb-3"
>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2">
<button
type="button"
onClick={() => condition.id && toggleGroupExpand(condition.id)}
className="p-1 hover:bg-accent rounded-sm"
>
{condition.id && expandedGroups.has(condition.id) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
<span className="text-sm font-medium">
Condition Group ({condition.logicalOperator})
</span>
</div>
<div className="flex items-center space-x-2" {...listeners}> <span className="text-sm font-medium">
<Move className="h-4 w-4 text-muted-foreground mr-2 cursor-grab" /> Condition Group ({condition.logicalOperator})
</span>
<Select
value={condition.logicalOperator || 'AND'}
onValueChange={(value) => updateCondition(index, 'logicalOperator', value)}
>
<SelectTrigger className="w-20 h-8">
<SelectValue placeholder="Operator" />
</SelectTrigger>
<SelectContent>
{logicalOperators.map((op) => (
<SelectItem key={op.value} value={op.value}>
{op.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="ghost"
size="icon"
onClick={() => addCondition(condition.id)}
title="Add Condition"
>
<Plus className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => addConditionGroup(condition.id)}
title="Add Group"
>
<FolderPlus className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => removeCondition(index)}
title="Remove Group"
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div> </div>
{condition.id && expandedGroups.has(condition.id) && ( <div className="flex items-center space-x-2">
<div className="pl-2"> <Select
{condition.id ? ( value={condition.logicalOperator || 'AND'}
(() => { onValueChange={(value) => updateCondition(index, 'logicalOperator', value)}
const childConditions = getConditionsForParent(condition.id); >
return childConditions.length === 0 ? ( <SelectTrigger className="w-20 h-8">
<div className="text-sm text-muted-foreground p-2"> <SelectValue placeholder="Operator" />
No conditions in this group. Add one using the buttons above. </SelectTrigger>
</div> <SelectContent>
) : ( {logicalOperators.map((op) => (
childConditions.map((child, childIndex) => { <SelectItem key={op.value} value={op.value}>
const originalIndex = conditions.findIndex(c => c === child); {op.label}
return <SortableItem </SelectItem>
key={`child-${originalIndex}`} ))}
condition={child} </SelectContent>
index={originalIndex} </Select>
level={level + 1}
/>; <Button
}) variant="ghost"
); size="icon"
})() onClick={() => addCondition(condition.id)}
) : null} title="Add Condition"
</div> >
)} <Plus className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => addConditionGroup(condition.id)}
title="Add Group"
>
<FolderPlus className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => removeCondition(index)}
title="Remove Group"
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div> </div>
) : (
renderConditionContent(condition, index, level, listeners) {condition.id && expandedGroups.has(condition.id) && (
)} <div className="pl-2">
</div> {renderChildConditions(condition.id, level + 1)}
); </div>
}; )}
// Render condition content (without the wrapper)
const renderConditionContent = (
condition: Condition,
index: number,
level = 0,
listeners?: ReturnType<typeof useSortable>['listeners']
) => {
return (
<div
className="grid grid-cols-13 gap-2 items-center mb-2 border border-transparent hover:border-border p-2 rounded-md"
>
<div
className="col-span-1 cursor-grab flex items-center justify-center"
{...listeners}
>
<Move className="h-4 w-4 text-muted-foreground" />
</div> </div>
{index > 0 && !condition.isGroup && condition.parentId === conditions[index-1].parentId && ( );
<div className="col-span-1"> } else {
// Render regular condition (not a group)
return (
<div
key={`condition-${index}`}
className="grid grid-cols-13 gap-2 items-center mb-2 border border-transparent hover:border-border p-2 rounded-md"
style={{ marginLeft: level > 0 ? `${level * 20}px` : '0' }}
>
<div className="col-span-1 flex items-center justify-center">
<Move className="h-4 w-4 text-muted-foreground" />
</div>
{index > 0 && !condition.isGroup && condition.parentId === conditions[index-1].parentId && (
<div className="col-span-1">
<Select
value={condition.logicalOperator || 'AND'}
onValueChange={(value) => updateCondition(index, 'logicalOperator', value)}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="AND" />
</SelectTrigger>
<SelectContent>
{logicalOperators.map((op) => (
<SelectItem key={op.value} value={op.value}>
{op.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{(index === 0 || condition.parentId !== conditions[index-1].parentId) && (
<div className="col-span-1"></div>
)}
<div className="col-span-4">
<Select <Select
value={condition.logicalOperator || 'AND'} value={condition.attribute}
onValueChange={(value) => updateCondition(index, 'logicalOperator', value)} onValueChange={(value) => updateCondition(index, 'attribute', value)}
> >
<SelectTrigger className="h-8"> <SelectTrigger>
<SelectValue placeholder="AND" /> <SelectValue placeholder="Select attribute" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{logicalOperators.map((op) => ( {availableAttributes.map((attr) => (
<SelectItem key={attr} value={attr}>
{attr}
</SelectItem>
))}
<SelectItem value="custom">Custom Attribute</SelectItem>
</SelectContent>
</Select>
{condition.attribute === 'custom' && (
<Input
className="mt-1"
placeholder="Enter custom attribute"
value={condition.customAttribute || ''}
onChange={(e) => updateCondition(index, 'customAttribute', e.target.value)}
/>
)}
</div>
<div className="col-span-3">
<Select
value={condition.operator}
onValueChange={(value) => updateCondition(index, 'operator', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select operator" />
</SelectTrigger>
<SelectContent>
{operators.map((op) => (
<SelectItem key={op.value} value={op.value}> <SelectItem key={op.value} value={op.value}>
{op.label} {op.label}
</SelectItem> </SelectItem>
@@ -427,80 +357,48 @@ const ConditionBuilder: React.FC<ConditionBuilderProps> = ({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)}
{(index === 0 || condition.parentId !== conditions[index-1].parentId) && (
<div className="col-span-1"></div>
)}
<div className="col-span-4">
<Select
value={condition.attribute}
onValueChange={(value) => updateCondition(index, 'attribute', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select attribute" />
</SelectTrigger>
<SelectContent>
{availableAttributes.map((attr) => (
<SelectItem key={attr} value={attr}>
{attr}
</SelectItem>
))}
<SelectItem value="custom">Custom Attribute</SelectItem>
</SelectContent>
</Select>
{condition.attribute === 'custom' && ( <div className="col-span-3">
<Input {condition.operator !== 'present' && condition.operator !== 'notPresent' && (
className="mt-1" <Input
placeholder="Enter custom attribute" placeholder="Value"
value={condition.customAttribute || ''} value={condition.value}
onChange={(e) => updateCondition(index, 'customAttribute', e.target.value)} onChange={(e) => updateCondition(index, 'value', e.target.value)}
/> />
)} )}
</div>
<div className="col-span-1 flex justify-end">
<Button
variant="ghost"
size="icon"
onClick={() => removeCondition(index)}
title="Remove Condition"
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div> </div>
);
<div className="col-span-3"> }
<Select };
value={condition.operator}
onValueChange={(value) => updateCondition(index, 'operator', value)} // Render all child conditions of a parent
> const renderChildConditions = (parentId: number, level = 0) => {
<SelectTrigger> const childConditions = getConditionsForParent(parentId);
<SelectValue placeholder="Select operator" />
</SelectTrigger> if (childConditions.length === 0) {
<SelectContent> return (
{operators.map((op) => ( <div className="text-sm text-muted-foreground p-2">
<SelectItem key={op.value} value={op.value}> No conditions in this group. Add one using the buttons above.
{op.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
);
<div className="col-span-3"> }
{condition.operator !== 'present' && condition.operator !== 'notPresent' && (
<Input return childConditions.map((condition, i) => {
placeholder="Value" const index = conditions.findIndex(c => c === condition);
value={condition.value} return renderCondition(condition, index, level);
onChange={(e) => updateCondition(index, 'value', e.target.value)} });
/>
)}
</div>
<div className="col-span-1 flex justify-end">
<Button
variant="ghost"
size="icon"
onClick={() => removeCondition(index)}
title="Remove Condition"
>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
);
}; };
return ( return (
@@ -531,25 +429,16 @@ const ConditionBuilder: React.FC<ConditionBuilderProps> = ({
</Card> </Card>
) : ( ) : (
<> <>
<DndContext <div className="space-y-2">
sensors={sensors} {/* Render root-level conditions */}
collisionDetection={closestCenter} {conditions
onDragEnd={handleDragEnd} .filter(c => c.parentId === null || c.parentId === undefined)
> .map((condition, i) => {
<SortableContext items={conditions.map(c => c.id || 0)}> const index = conditions.findIndex(c => c === condition);
<div className="space-y-2"> return renderCondition(condition, index, 0);
{conditions.map((condition, index) => ( })
condition.parentId === null || condition.parentId === undefined ? ( }
<SortableItem </div>
key={`root-${index}`}
condition={condition}
index={index}
/>
) : null
))}
</div>
</SortableContext>
</DndContext>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button <Button