128 lines
4.1 KiB
TypeScript
128 lines
4.1 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 '../nodes/shared/constants';
|
|
import { getLabel } from '../nodes/shared/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;
|
|
/** Hide palette categories (e.g. trigger — start node comes from workflow config only) */
|
|
excludedCategories?: Set<string>;
|
|
}
|
|
|
|
export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
|
nodeTypes,
|
|
categories,
|
|
filter,
|
|
onFilterChange,
|
|
language,
|
|
expandedCategories,
|
|
onToggleCategory,
|
|
excludedCategories,
|
|
}) => {
|
|
const filteredNodeTypes = useMemo(() => {
|
|
const visible = nodeTypes.filter(
|
|
(n) =>
|
|
!HIDDEN_NODE_IDS.has(n.id) &&
|
|
!(excludedCategories?.has(n.category || ''))
|
|
);
|
|
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>
|
|
);
|
|
};
|