mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-27 19:17:07 +00:00
Fix: Resolved broken focus and filter generation in dynamic group management. Improved condition builder component for better user experience.
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/d81ff9b2-e4c8-4415-b4de-8f7c4be29888.jpg
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@@ -394,24 +394,13 @@ const ConditionBuilder: React.FC<ConditionBuilderProps> = ({
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{condition.attribute === 'custom' && (
|
{condition.attribute === 'custom' && (
|
||||||
<Input
|
<TextInput
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
placeholder="Enter custom attribute"
|
placeholder="Enter custom attribute"
|
||||||
value={condition.customAttribute || ''}
|
value={condition.customAttribute || ''}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
// Create direct reference to prevent re-rendering that causes focus loss
|
updateCondition(index, 'customAttribute', value);
|
||||||
const newConditions = [...conditions];
|
|
||||||
newConditions[index] = {
|
|
||||||
...newConditions[index],
|
|
||||||
customAttribute: e.target.value
|
|
||||||
};
|
|
||||||
// Use a stable reference - in React 18+ this helps maintain focus
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
onChange(newConditions);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
// Add key to ensure input identity is stable
|
|
||||||
key={`custom-attr-input-${condition.id || index}`}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -436,23 +425,12 @@ const ConditionBuilder: React.FC<ConditionBuilderProps> = ({
|
|||||||
|
|
||||||
<div className="col-span-3">
|
<div className="col-span-3">
|
||||||
{condition.operator !== 'present' && condition.operator !== 'notPresent' && (
|
{condition.operator !== 'present' && condition.operator !== 'notPresent' && (
|
||||||
<Input
|
<TextInput
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
value={condition.value || ''}
|
value={condition.value || ''}
|
||||||
onChange={(e) => {
|
onChange={(value) => {
|
||||||
// Create direct reference to prevent re-rendering that causes focus loss
|
updateCondition(index, 'value', value);
|
||||||
const newConditions = [...conditions];
|
|
||||||
newConditions[index] = {
|
|
||||||
...newConditions[index],
|
|
||||||
value: e.target.value
|
|
||||||
};
|
|
||||||
// Use a stable reference - in React 18+ this helps maintain focus
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
onChange(newConditions);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
// Add key to ensure input identity is stable
|
|
||||||
key={`value-input-${condition.id || index}`}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,694 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Trash, Plus, Move, FolderPlus, ChevronDown, ChevronRight, Copy } from 'lucide-react';
|
||||||
|
import { Condition } from './rule-editor';
|
||||||
|
import { LdapAttribute } from '@shared/schema';
|
||||||
|
|
||||||
|
interface ConditionBuilderProps {
|
||||||
|
conditions: Condition[];
|
||||||
|
onChange: (conditions: Condition[]) => void;
|
||||||
|
connections: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoized component to maintain focus during renders
|
||||||
|
const ConditionValue = React.memo(({ value, onChange, placeholder }: {
|
||||||
|
value: string,
|
||||||
|
onChange: (value: string) => void,
|
||||||
|
placeholder?: string
|
||||||
|
}) => {
|
||||||
|
const [localValue, setLocalValue] = useState(value || '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== localValue) {
|
||||||
|
setLocalValue(value || '');
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
value={localValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ConditionValue.displayName = 'ConditionValue';
|
||||||
|
|
||||||
|
// Memoized component for custom attribute input
|
||||||
|
const CustomAttributeInput = React.memo(({ value, onChange, placeholder }: {
|
||||||
|
value: string,
|
||||||
|
onChange: (value: string) => void,
|
||||||
|
placeholder?: string
|
||||||
|
}) => {
|
||||||
|
const [localValue, setLocalValue] = useState(value || '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== localValue) {
|
||||||
|
setLocalValue(value || '');
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
setLocalValue(newValue);
|
||||||
|
onChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className="mt-1"
|
||||||
|
value={localValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CustomAttributeInput.displayName = 'CustomAttributeInput';
|
||||||
|
|
||||||
|
const operators = [
|
||||||
|
{ value: '=', label: 'Equals' },
|
||||||
|
{ value: '!=', label: 'Not Equals' },
|
||||||
|
{ value: 'contains', label: 'Contains' },
|
||||||
|
{ value: 'startsWith', label: 'Starts With' },
|
||||||
|
{ value: 'endsWith', label: 'Ends With' },
|
||||||
|
{ value: 'present', label: 'Is Present' },
|
||||||
|
{ value: 'notPresent', label: 'Is Not Present' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const logicalOperators = [
|
||||||
|
{ value: 'AND', label: 'AND' },
|
||||||
|
{ value: 'OR', label: 'OR' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultCondition: Condition = {
|
||||||
|
attribute: '',
|
||||||
|
operator: '=',
|
||||||
|
value: '',
|
||||||
|
logicalOperator: 'AND'
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultGroup: Condition = {
|
||||||
|
attribute: '',
|
||||||
|
operator: '',
|
||||||
|
value: '',
|
||||||
|
logicalOperator: 'AND',
|
||||||
|
isGroup: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Non-recursive function to generate LDAP filter syntax from conditions
|
||||||
|
const generateLdapFilter = (conditions: Condition[]): string => {
|
||||||
|
if (conditions.length === 0) return '(objectClass=*)';
|
||||||
|
|
||||||
|
// Helper function to generate filter for a regular condition (not a group)
|
||||||
|
const generateRegularFilter = (condition: Condition): string => {
|
||||||
|
// Get actual attribute name (custom or selected)
|
||||||
|
const attributeName = condition.attribute === 'custom' && condition.customAttribute
|
||||||
|
? condition.customAttribute
|
||||||
|
: condition.attribute;
|
||||||
|
|
||||||
|
if (!attributeName) return '';
|
||||||
|
|
||||||
|
// Regular condition
|
||||||
|
switch (condition.operator) {
|
||||||
|
case '=':
|
||||||
|
return `(${attributeName}=${condition.value})`;
|
||||||
|
case '!=':
|
||||||
|
return `(!(${attributeName}=${condition.value}))`;
|
||||||
|
case 'contains':
|
||||||
|
return `(${attributeName}=*${condition.value}*)`;
|
||||||
|
case 'startsWith':
|
||||||
|
return `(${attributeName}=${condition.value}*)`;
|
||||||
|
case 'endsWith':
|
||||||
|
return `(${attributeName}=*${condition.value})`;
|
||||||
|
case 'present':
|
||||||
|
return `(${attributeName}=*)`;
|
||||||
|
case 'notPresent':
|
||||||
|
return `(!(${attributeName}=*))`;
|
||||||
|
default:
|
||||||
|
return `(${attributeName}=${condition.value})`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build a map of parent IDs to their child conditions
|
||||||
|
const childrenByParentId = new Map<number | null | undefined, Condition[]>();
|
||||||
|
|
||||||
|
// Organize conditions by their parent
|
||||||
|
for (const condition of conditions) {
|
||||||
|
const parentId = condition.parentId;
|
||||||
|
if (!childrenByParentId.has(parentId)) {
|
||||||
|
childrenByParentId.set(parentId, []);
|
||||||
|
}
|
||||||
|
childrenByParentId.get(parentId)!.push(condition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate filter for a group using an iterative approach
|
||||||
|
const generateGroupFilter = (parentId: number | null | undefined): string => {
|
||||||
|
const childConditions = childrenByParentId.get(parentId) || [];
|
||||||
|
if (childConditions.length === 0) return '(objectClass=*)';
|
||||||
|
|
||||||
|
// Find the logical operator for this group
|
||||||
|
const logicalOp = parentId === null || parentId === undefined
|
||||||
|
? 'AND' // Default for root level
|
||||||
|
: conditions.find(c => c.id === parentId)?.logicalOperator || 'AND';
|
||||||
|
|
||||||
|
const operator = logicalOp === 'AND' ? '&' : '|';
|
||||||
|
let groupFilter = `(${operator}`;
|
||||||
|
|
||||||
|
// Track if we've added any conditions to this group
|
||||||
|
let hasAddedCondition = false;
|
||||||
|
|
||||||
|
// Process regular conditions in this group
|
||||||
|
for (const condition of childConditions) {
|
||||||
|
if (!condition.isGroup) {
|
||||||
|
const filter = generateRegularFilter(condition);
|
||||||
|
if (filter) {
|
||||||
|
groupFilter += filter;
|
||||||
|
hasAddedCondition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process subgroups in this group
|
||||||
|
for (const condition of childConditions) {
|
||||||
|
if (condition.isGroup && condition.id !== undefined) {
|
||||||
|
const subGroupFilter = generateGroupFilter(condition.id);
|
||||||
|
if (subGroupFilter !== '(objectClass=*)') {
|
||||||
|
groupFilter += subGroupFilter;
|
||||||
|
hasAddedCondition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupFilter += ')';
|
||||||
|
|
||||||
|
// If there are no actual conditions, return a default
|
||||||
|
if (!hasAddedCondition) {
|
||||||
|
return '(objectClass=*)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupFilter;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start with the root level conditions
|
||||||
|
return generateGroupFilter(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Main component (completely rewritten)
|
||||||
|
const FixedConditionBuilder: React.FC<ConditionBuilderProps> = ({
|
||||||
|
conditions,
|
||||||
|
onChange,
|
||||||
|
connections
|
||||||
|
}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [expandedGroups, setExpandedGroups] = useState<Set<number>>(new Set());
|
||||||
|
const [ldapFilter, setLdapFilter] = useState('');
|
||||||
|
|
||||||
|
// Load LDAP attributes from connections
|
||||||
|
const { data: attributesData = [] } = useQuery<LdapAttribute[]>({
|
||||||
|
queryKey: ['/api/ldap-attributes', { connectionIds: connections }],
|
||||||
|
enabled: connections.length > 0,
|
||||||
|
staleTime: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [availableAttributes, setAvailableAttributes] = useState<string[]>([
|
||||||
|
'cn', 'sAMAccountName', 'givenName', 'sn', 'mail', 'displayName',
|
||||||
|
'memberOf', 'distinguishedName', 'objectClass', 'objectCategory',
|
||||||
|
'userAccountControl', 'department', 'company', 'title', 'manager'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update available attributes when data is loaded
|
||||||
|
useEffect(() => {
|
||||||
|
if (attributesData.length > 0) {
|
||||||
|
// Extract unique attribute names
|
||||||
|
const uniqueAttributes = Array.from(
|
||||||
|
new Set(attributesData.map(attr => attr.name))
|
||||||
|
);
|
||||||
|
setAvailableAttributes(uniqueAttributes);
|
||||||
|
}
|
||||||
|
}, [attributesData]);
|
||||||
|
|
||||||
|
// Update LDAP filter when conditions change
|
||||||
|
useEffect(() => {
|
||||||
|
setLdapFilter(generateLdapFilter(conditions));
|
||||||
|
}, [conditions]);
|
||||||
|
|
||||||
|
// Handle adding a new condition
|
||||||
|
const addCondition = (parentId?: number | null) => {
|
||||||
|
const newCondition: Condition = {
|
||||||
|
...defaultCondition,
|
||||||
|
id: Math.max(0, ...conditions.map(c => c.id || 0)) + 1, // Generate a new ID
|
||||||
|
parentId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the appropriate insertion position
|
||||||
|
if (parentId === undefined) {
|
||||||
|
// Add to root level
|
||||||
|
onChange([...conditions, newCondition]);
|
||||||
|
} else {
|
||||||
|
// Find the last condition with the same parent to add it after
|
||||||
|
const sameParentConditions = conditions.filter(c => c.parentId === parentId);
|
||||||
|
const insertIndex = sameParentConditions.length > 0
|
||||||
|
? conditions.indexOf(sameParentConditions[sameParentConditions.length - 1]) + 1
|
||||||
|
: conditions.findIndex(c => c.id === parentId) + 1;
|
||||||
|
|
||||||
|
const newConditions = [...conditions];
|
||||||
|
newConditions.splice(insertIndex, 0, newCondition);
|
||||||
|
onChange(newConditions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle adding a new condition group
|
||||||
|
const addConditionGroup = (parentId?: number | null) => {
|
||||||
|
// Generate a new ID for the group
|
||||||
|
const newId = Math.max(0, ...conditions.map(c => c.id || 0)) + 1;
|
||||||
|
|
||||||
|
const newGroup: Condition = {
|
||||||
|
...defaultGroup,
|
||||||
|
id: newId, // Assign the new ID
|
||||||
|
parentId
|
||||||
|
};
|
||||||
|
|
||||||
|
// Similar insertion logic as addCondition
|
||||||
|
if (parentId === undefined) {
|
||||||
|
onChange([...conditions, newGroup]);
|
||||||
|
} else {
|
||||||
|
const sameParentConditions = conditions.filter(c => c.parentId === parentId);
|
||||||
|
const insertIndex = sameParentConditions.length > 0
|
||||||
|
? conditions.indexOf(sameParentConditions[sameParentConditions.length - 1]) + 1
|
||||||
|
: conditions.findIndex(c => c.id === parentId) + 1;
|
||||||
|
|
||||||
|
const newConditions = [...conditions];
|
||||||
|
newConditions.splice(insertIndex, 0, newGroup);
|
||||||
|
onChange(newConditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Automatically expand the new group
|
||||||
|
setExpandedGroups(prev => {
|
||||||
|
const expanded = new Set(prev);
|
||||||
|
expanded.add(newId);
|
||||||
|
return expanded;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle updating a condition
|
||||||
|
const updateCondition = (index: number, field: keyof Condition, value: any) => {
|
||||||
|
const newConditions = [...conditions];
|
||||||
|
newConditions[index] = { ...newConditions[index], [field]: value };
|
||||||
|
onChange(newConditions);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle removing a condition or group
|
||||||
|
const removeCondition = (index: number) => {
|
||||||
|
const conditionToRemove = conditions[index];
|
||||||
|
|
||||||
|
// If it's a group, also check for child conditions
|
||||||
|
if (conditionToRemove.isGroup) {
|
||||||
|
const childConditions = conditions.filter(c => c.parentId === conditionToRemove.id);
|
||||||
|
|
||||||
|
if (childConditions.length > 0) {
|
||||||
|
const confirmDelete = window.confirm(
|
||||||
|
`This will also delete ${childConditions.length} child condition(s). Continue?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we have a valid ID before proceeding
|
||||||
|
if (conditionToRemove.id !== undefined) {
|
||||||
|
// Find just this group's children (not all groups with the same parent)
|
||||||
|
const childrenToRemove = getAllDescendants(conditionToRemove.id);
|
||||||
|
|
||||||
|
// Create a list of IDs to remove (the group and all its descendants)
|
||||||
|
const idsToRemove = new Set<number | undefined>([
|
||||||
|
conditionToRemove.id,
|
||||||
|
...childrenToRemove.map(c => c.id).filter((id): id is number => id !== undefined)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Only remove this specific group and its descendants
|
||||||
|
const newConditions = conditions.filter(c => {
|
||||||
|
// Keep the condition if its ID is not in our removal list
|
||||||
|
// and its parent ID is not in our removal list
|
||||||
|
return !idsToRemove.has(c.id) &&
|
||||||
|
!(c.parentId !== null && idsToRemove.has(c.parentId));
|
||||||
|
});
|
||||||
|
|
||||||
|
onChange(newConditions);
|
||||||
|
} else {
|
||||||
|
// No valid ID, just remove the condition at this index
|
||||||
|
const newConditions = conditions.filter((c, i) => i !== index);
|
||||||
|
onChange(newConditions);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Just remove the single condition
|
||||||
|
const newConditions = conditions.filter((c, i) => i !== index);
|
||||||
|
onChange(newConditions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get all descendants of a condition group
|
||||||
|
const getAllDescendants = (parentId: number | undefined): Condition[] => {
|
||||||
|
if (parentId === undefined) return [];
|
||||||
|
|
||||||
|
const directChildren = conditions.filter(c => c.parentId === parentId);
|
||||||
|
|
||||||
|
// Get all children of groups within this group (recursively)
|
||||||
|
const groupChildren = directChildren
|
||||||
|
.filter(c => c.isGroup && c.id !== undefined)
|
||||||
|
.flatMap(group => getAllDescendants(group.id));
|
||||||
|
|
||||||
|
return [...directChildren, ...groupChildren];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle group expansion
|
||||||
|
const toggleGroupExpand = (groupId: number) => {
|
||||||
|
setExpandedGroups(prev => {
|
||||||
|
const newExpanded = new Set(prev);
|
||||||
|
if (newExpanded.has(groupId)) {
|
||||||
|
newExpanded.delete(groupId);
|
||||||
|
} else {
|
||||||
|
newExpanded.add(groupId);
|
||||||
|
}
|
||||||
|
return newExpanded;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render a single condition (regular or group)
|
||||||
|
const renderCondition = (condition: Condition, index: number, level = 0) => {
|
||||||
|
// If this is a group condition
|
||||||
|
if (condition.isGroup) {
|
||||||
|
// Get all direct children of this group
|
||||||
|
const childConditions = condition.id
|
||||||
|
? conditions.filter(c => c.parentId === condition.id)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const isExpanded = condition.id ? expandedGroups.has(condition.id) : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3" key={`group-${condition.id || index}`}>
|
||||||
|
<div
|
||||||
|
className={`border ${isExpanded ? 'bg-background/50' : 'bg-accent/10'} border-border rounded-md p-3`}
|
||||||
|
style={{ marginLeft: level > 0 ? `${level * 20}px` : '0' }}
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{isExpanded ? (
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<div className="pl-2">
|
||||||
|
{childConditions.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
{childConditions.map((childCondition, i) => {
|
||||||
|
const childIndex = conditions.indexOf(childCondition);
|
||||||
|
return renderCondition(childCondition, childIndex, level + 1);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-muted-foreground p-2">
|
||||||
|
No conditions in this group. Add one using the buttons above.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular condition
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`condition-${condition.id || index}`}
|
||||||
|
className="grid grid-cols-13 gap-2 items-center mb-2 border border-transparent hover:border-border p-2 rounded-md bg-background/50 hover:bg-background"
|
||||||
|
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
|
||||||
|
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' && (
|
||||||
|
<CustomAttributeInput
|
||||||
|
placeholder="Enter custom attribute"
|
||||||
|
value={condition.customAttribute || ''}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateCondition(index, 'customAttribute', 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}>
|
||||||
|
{op.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-3">
|
||||||
|
{condition.operator !== 'present' && condition.operator !== 'notPresent' && (
|
||||||
|
<ConditionValue
|
||||||
|
placeholder="Value"
|
||||||
|
value={condition.value || ''}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateCondition(index, 'value', 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get top-level conditions
|
||||||
|
const rootConditions = conditions.filter(c =>
|
||||||
|
c.parentId === null || c.parentId === undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{conditions.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6 flex flex-col items-center justify-center">
|
||||||
|
<p className="text-muted-foreground mb-4">No conditions defined yet</p>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => addCondition()}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Condition
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => addConditionGroup()}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<FolderPlus className="mr-2 h-4 w-4" />
|
||||||
|
Add Condition Group
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{rootConditions.map((condition, i) => {
|
||||||
|
const index = conditions.indexOf(condition);
|
||||||
|
return renderCondition(condition, index);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => addCondition()}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Add Condition
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => addConditionGroup()}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
<FolderPlus className="mr-2 h-4 w-4" />
|
||||||
|
Add Condition Group
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 p-4 bg-muted rounded-md">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<Label className="text-sm font-medium">Generated LDAP Filter:</Label>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(ldapFilter);
|
||||||
|
toast({
|
||||||
|
title: "LDAP Filter Copied!",
|
||||||
|
description: "The LDAP filter has been copied to your clipboard.",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-3.5 w-3.5 mr-1" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="font-mono text-sm overflow-auto p-2 bg-background rounded border whitespace-pre-wrap">
|
||||||
|
{ldapFilter}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FixedConditionBuilder;
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
|
|
||||||
import { DynamicGroupRule, LdapConnection } from '@shared/schema';
|
import { DynamicGroupRule, LdapConnection } from '@shared/schema';
|
||||||
import CronJobBuilder, { ScheduleItem } from './cron-job-builder';
|
import CronJobBuilder, { ScheduleItem } from './cron-job-builder';
|
||||||
import ConditionBuilder from './condition-builder';
|
import FixedConditionBuilder from './fixed-condition-builder';
|
||||||
import VariableSelector from './variable-selector';
|
import VariableSelector from './variable-selector';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { queryClient, apiRequest } from '@/lib/queryClient';
|
import { queryClient, apiRequest } from '@/lib/queryClient';
|
||||||
|
|||||||
Reference in New Issue
Block a user