frontend_nyla/src/components/Automation2FlowEditor/NodeSidebar.tsx
idittrich-valueon 896f7b5968 sharepoint nodes
2026-03-22 19:50:06 +01:00

121 lines
3.9 KiB
TypeScript

/**
* NodeSidebar - Sidebar with searchable, collapsible node list.
* Groups node types by category (trigger, input, flow, data, ai, email, sharepoint).
*/
import React, { useMemo } from 'react';
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
import type { NodeType, NodeTypeCategory } from '../../api/automation2Api';
import { CATEGORY_ORDER, HIDDEN_NODE_IDS } from './constants';
import { getLabel } from './utils';
import { NodeListItem } from './NodeListItem';
import styles from './Automation2FlowEditor.module.css';
interface NodeSidebarProps {
nodeTypes: NodeType[];
categories: NodeTypeCategory[];
filter: string;
onFilterChange: (value: string) => void;
language: string;
expandedCategories: Set<string>;
onToggleCategory: (id: string) => void;
}
export const NodeSidebar: React.FC<NodeSidebarProps> = ({
nodeTypes,
categories,
filter,
onFilterChange,
language,
expandedCategories,
onToggleCategory,
}) => {
const filteredNodeTypes = useMemo(() => {
const visible = nodeTypes.filter((n) => !HIDDEN_NODE_IDS.has(n.id));
if (!filter.trim()) return visible;
const q = filter.toLowerCase();
return visible.filter(
(n) =>
n.id.toLowerCase().includes(q) ||
getLabel(n.label, language).toLowerCase().includes(q) ||
getLabel(n.description, language).toLowerCase().includes(q)
);
}, [nodeTypes, filter, language]);
const groupedByCategory = useMemo(() => {
const map: Record<string, NodeType[]> = {};
filteredNodeTypes.forEach((n) => {
const cat = n.category || 'other';
if (!map[cat]) map[cat] = [];
map[cat].push(n);
});
return map;
}, [filteredNodeTypes]);
const orderedCategories = useMemo(() => {
const seen = new Set<string>();
const result: string[] = [];
CATEGORY_ORDER.forEach((id) => {
if (groupedByCategory[id]) {
result.push(id);
seen.add(id);
}
});
Object.keys(groupedByCategory).forEach((id) => {
if (!seen.has(id)) result.push(id);
});
return result;
}, [groupedByCategory]);
const getLabelFn = (t: string | Record<string, string> | undefined, lang?: string) =>
getLabel(t, lang ?? language);
return (
<div className={styles.sidebar}>
<div className={styles.sidebarHeader}>
<h3 className={styles.sidebarTitle}>Nodes</h3>
<input
type="text"
className={styles.sidebarSearch}
placeholder="Nodes durchsuchen..."
value={filter}
onChange={(e) => onFilterChange(e.target.value)}
/>
</div>
<div className={styles.nodeList}>
{orderedCategories.map((catId) => {
const isExpanded = expandedCategories.has(catId);
const catLabel = categories.find((c) => c.id === catId);
const label = getLabel(catLabel?.label, language) || catId;
const items = groupedByCategory[catId] || [];
return (
<div key={catId} className={styles.categoryGroup}>
<button
type="button"
className={styles.categoryHeader}
onClick={() => onToggleCategory(catId)}
>
{isExpanded ? (
<FaChevronDown className={styles.categoryIcon} />
) : (
<FaChevronRight className={styles.categoryIcon} />
)}
<span className={styles.categoryLabel}>{label}</span>
<span className={styles.categoryCount}>{items.length}</span>
</button>
{isExpanded &&
items.map((node) => (
<NodeListItem
key={node.id}
node={node}
language={language}
getLabel={getLabelFn}
/>
))}
</div>
);
})}
</div>
</div>
);
};