mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-27 19:17:07 +00:00
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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user