finished email nodes
This commit is contained in:
parent
835428ac5f
commit
af58d5a868
12 changed files with 631 additions and 115 deletions
|
|
@ -289,3 +289,68 @@ export async function completeTask(
|
||||||
data: { result },
|
data: { result },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Connections and Browse (for Email/SharePoint node config)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface UserConnection {
|
||||||
|
id: string;
|
||||||
|
authority: string;
|
||||||
|
externalUsername?: string;
|
||||||
|
externalEmail?: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchConnections(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
instanceId: string
|
||||||
|
): Promise<UserConnection[]> {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/automation2/${instanceId}/connections`,
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
return data?.connections ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectionService {
|
||||||
|
service: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchConnectionServices(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
instanceId: string,
|
||||||
|
connectionId: string
|
||||||
|
): Promise<ConnectionService[]> {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/automation2/${instanceId}/connections/${connectionId}/services`,
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
return data?.services ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BrowseEntry {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
isFolder: boolean;
|
||||||
|
size?: number;
|
||||||
|
mimeType?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchBrowse(
|
||||||
|
request: ApiRequestFunction,
|
||||||
|
instanceId: string,
|
||||||
|
connectionId: string,
|
||||||
|
service: string,
|
||||||
|
path = '/'
|
||||||
|
): Promise<{ items: BrowseEntry[]; path: string; service: string }> {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/automation2/${instanceId}/connections/${connectionId}/browse`,
|
||||||
|
method: 'get',
|
||||||
|
params: { service, path },
|
||||||
|
});
|
||||||
|
return { items: data?.items ?? [], path: data?.path ?? path, service: data?.service ?? service };
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,8 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||||
new Set(['trigger', 'input', 'flow', 'data'])
|
new Set(['trigger', 'input', 'flow', 'data', 'ai', 'email', 'sharepoint'])
|
||||||
);
|
);
|
||||||
const [expandedIoMethods, setExpandedIoMethods] = useState<Set<string>>(new Set());
|
|
||||||
const [canvasNodes, setCanvasNodes] = useState<CanvasNode[]>([]);
|
const [canvasNodes, setCanvasNodes] = useState<CanvasNode[]>([]);
|
||||||
const [canvasConnections, setCanvasConnections] = useState<CanvasConnection[]>([]);
|
const [canvasConnections, setCanvasConnections] = useState<CanvasConnection[]>([]);
|
||||||
const [executing, setExecuting] = useState(false);
|
const [executing, setExecuting] = useState(false);
|
||||||
|
|
@ -208,15 +207,6 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const toggleIoMethod = useCallback((method: string) => {
|
|
||||||
setExpandedIoMethods((prev) => {
|
|
||||||
const next = new Set(prev);
|
|
||||||
if (next.has(method)) next.delete(method);
|
|
||||||
else next.add(method);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDropNodeType = useCallback(
|
const handleDropNodeType = useCallback(
|
||||||
(nodeTypeId: string, x: number, y: number) => {
|
(nodeTypeId: string, x: number, y: number) => {
|
||||||
const nt = nodeTypes.find((n) => n.id === nodeTypeId);
|
const nt = nodeTypes.find((n) => n.id === nodeTypeId);
|
||||||
|
|
@ -280,9 +270,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
|
||||||
onFilterChange={setFilter}
|
onFilterChange={setFilter}
|
||||||
language={language}
|
language={language}
|
||||||
expandedCategories={expandedCategories}
|
expandedCategories={expandedCategories}
|
||||||
expandedIoMethods={expandedIoMethods}
|
|
||||||
onToggleCategory={toggleCategory}
|
onToggleCategory={toggleCategory}
|
||||||
onToggleIoMethod={toggleIoMethod}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -318,12 +306,17 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
|
||||||
onSelectionChange={setSelectedNode}
|
onSelectionChange={setSelectedNode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{selectedNode?.type?.startsWith('input.') && (
|
{selectedNode &&
|
||||||
|
['input.', 'ai.', 'email.', 'sharepoint.'].some((p) =>
|
||||||
|
selectedNode.type.startsWith(p)
|
||||||
|
) && (
|
||||||
<NodeConfigPanel
|
<NodeConfigPanel
|
||||||
node={selectedNode}
|
node={selectedNode}
|
||||||
nodeType={nodeTypes.find((nt) => nt.id === selectedNode.type)}
|
nodeType={nodeTypes.find((nt) => nt.id === selectedNode.type)}
|
||||||
language={language}
|
language={language}
|
||||||
onParametersChange={handleNodeParametersChange}
|
onParametersChange={handleNodeParametersChange}
|
||||||
|
instanceId={instanceId}
|
||||||
|
request={request}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* NodeConfigPanel - Configures parameters for input/human nodes.
|
* NodeConfigPanel - Configures parameters for input, ai, email, sharepoint nodes.
|
||||||
* Delegates to config components from configs/.
|
* Delegates to config components from configs/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import type { CanvasNode } from './FlowCanvas';
|
import type { CanvasNode } from './FlowCanvas';
|
||||||
import type { NodeType } from '../../api/automation2Api';
|
import type { NodeType } from '../../api/automation2Api';
|
||||||
|
import type { ApiRequestFunction } from '../../api/automation2Api';
|
||||||
import { getLabel } from './utils';
|
import { getLabel } from './utils';
|
||||||
import { NODE_CONFIG_REGISTRY } from './configs';
|
import { NODE_CONFIG_REGISTRY } from './configs';
|
||||||
import styles from './Automation2FlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
@ -15,13 +16,19 @@ interface NodeConfigPanelProps {
|
||||||
nodeType: NodeType | undefined;
|
nodeType: NodeType | undefined;
|
||||||
language: string;
|
language: string;
|
||||||
onParametersChange: (nodeId: string, parameters: Record<string, unknown>) => void;
|
onParametersChange: (nodeId: string, parameters: Record<string, unknown>) => void;
|
||||||
|
instanceId?: string;
|
||||||
|
request?: ApiRequestFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONFIGURABLE_PREFIXES = ['input.', 'ai.', 'email.', 'sharepoint.'];
|
||||||
|
|
||||||
export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({
|
export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({
|
||||||
node,
|
node,
|
||||||
nodeType,
|
nodeType,
|
||||||
language,
|
language,
|
||||||
onParametersChange,
|
onParametersChange,
|
||||||
|
instanceId,
|
||||||
|
request,
|
||||||
}) => {
|
}) => {
|
||||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||||
|
|
||||||
|
|
@ -35,14 +42,15 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({
|
||||||
if (node) onParametersChange(node.id, next);
|
if (node) onParametersChange(node.id, next);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!node || !node.type.startsWith('input.')) return null;
|
const isConfigurable = node && CONFIGURABLE_PREFIXES.some((p) => node.type.startsWith(p));
|
||||||
|
if (!node || !isConfigurable) return null;
|
||||||
|
|
||||||
const ConfigRenderer = NODE_CONFIG_REGISTRY[node.type];
|
const ConfigRenderer = NODE_CONFIG_REGISTRY[node.type];
|
||||||
if (!ConfigRenderer) {
|
if (!ConfigRenderer) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.nodeConfigPanel}>
|
<div className={styles.nodeConfigPanel}>
|
||||||
<h4>{getLabel(nodeType?.label, language) || node.type}</h4>
|
<h4>{getLabel(nodeType?.label, language) || node.type}</h4>
|
||||||
<p>Keine Konfiguration für {node.type}</p>
|
<p>No configuration for {node.type}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +58,13 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className={styles.nodeConfigPanel}>
|
<div className={styles.nodeConfigPanel}>
|
||||||
<h4>{getLabel(nodeType?.label, language) || node.type}</h4>
|
<h4>{getLabel(nodeType?.label, language) || node.type}</h4>
|
||||||
<ConfigRenderer params={params} updateParam={updateParam} />
|
<ConfigRenderer
|
||||||
|
params={params}
|
||||||
|
updateParam={updateParam}
|
||||||
|
instanceId={instanceId}
|
||||||
|
request={request}
|
||||||
|
nodeType={node.type}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* NodeSidebar - Sidebar with searchable, collapsible node list.
|
* NodeSidebar - Sidebar with searchable, collapsible node list.
|
||||||
* Groups node types by category; I/O nodes are sub-grouped by method.
|
* Groups node types by category (trigger, input, flow, data, ai, email, sharepoint).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
|
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
|
||||||
import type { NodeType, NodeTypeCategory } from '../../api/automation2Api';
|
import type { NodeType, NodeTypeCategory } from '../../api/automation2Api';
|
||||||
import { IO_METHOD_ORDER, CATEGORY_ORDER } from './constants';
|
import { CATEGORY_ORDER } from './constants';
|
||||||
import { getLabel, getIoMethodLabel } from './utils';
|
import { getLabel } from './utils';
|
||||||
import { NodeListItem } from './NodeListItem';
|
import { NodeListItem } from './NodeListItem';
|
||||||
import styles from './Automation2FlowEditor.module.css';
|
import styles from './Automation2FlowEditor.module.css';
|
||||||
|
|
||||||
|
|
@ -18,9 +18,7 @@ interface NodeSidebarProps {
|
||||||
onFilterChange: (value: string) => void;
|
onFilterChange: (value: string) => void;
|
||||||
language: string;
|
language: string;
|
||||||
expandedCategories: Set<string>;
|
expandedCategories: Set<string>;
|
||||||
expandedIoMethods: Set<string>;
|
|
||||||
onToggleCategory: (id: string) => void;
|
onToggleCategory: (id: string) => void;
|
||||||
onToggleIoMethod: (method: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
||||||
|
|
@ -30,9 +28,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
language,
|
language,
|
||||||
expandedCategories,
|
expandedCategories,
|
||||||
expandedIoMethods,
|
|
||||||
onToggleCategory,
|
onToggleCategory,
|
||||||
onToggleIoMethod,
|
|
||||||
}) => {
|
}) => {
|
||||||
const filteredNodeTypes = useMemo(() => {
|
const filteredNodeTypes = useMemo(() => {
|
||||||
if (!filter.trim()) return nodeTypes;
|
if (!filter.trim()) return nodeTypes;
|
||||||
|
|
@ -55,25 +51,6 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
||||||
return map;
|
return map;
|
||||||
}, [filteredNodeTypes]);
|
}, [filteredNodeTypes]);
|
||||||
|
|
||||||
const ioSubGroups = useMemo(() => {
|
|
||||||
const ioNodes = groupedByCategory['io'] || [];
|
|
||||||
const byMethod: Record<string, NodeType[]> = {};
|
|
||||||
ioNodes.forEach((n) => {
|
|
||||||
const method = n.meta?.method ?? n.id.split('.')[1] ?? 'other';
|
|
||||||
if (!byMethod[method]) byMethod[method] = [];
|
|
||||||
byMethod[method].push(n);
|
|
||||||
});
|
|
||||||
const ordered: Array<{ method: string; nodes: NodeType[] }> = [];
|
|
||||||
const methodOrder = [...IO_METHOD_ORDER];
|
|
||||||
methodOrder.forEach((m) => {
|
|
||||||
if (byMethod[m]?.length) ordered.push({ method: m, nodes: byMethod[m] });
|
|
||||||
});
|
|
||||||
Object.keys(byMethod).forEach((m) => {
|
|
||||||
if (!methodOrder.includes(m)) ordered.push({ method: m, nodes: byMethod[m] });
|
|
||||||
});
|
|
||||||
return ordered;
|
|
||||||
}, [groupedByCategory]);
|
|
||||||
|
|
||||||
const orderedCategories = useMemo(() => {
|
const orderedCategories = useMemo(() => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
|
|
@ -109,44 +86,6 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
||||||
const isExpanded = expandedCategories.has(catId);
|
const isExpanded = expandedCategories.has(catId);
|
||||||
const catLabel = categories.find((c) => c.id === catId);
|
const catLabel = categories.find((c) => c.id === catId);
|
||||||
const label = getLabel(catLabel?.label, language) || catId;
|
const label = getLabel(catLabel?.label, language) || catId;
|
||||||
|
|
||||||
if (catId === 'io' && ioSubGroups.length > 0) {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={catId}>
|
|
||||||
{ioSubGroups.map(({ method, nodes }) => {
|
|
||||||
const methodLabel = getIoMethodLabel(method, language);
|
|
||||||
const isMethodExpanded = expandedIoMethods.has(method);
|
|
||||||
return (
|
|
||||||
<div key={`io-${method}`} className={styles.categoryGroup}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.categoryHeader}
|
|
||||||
onClick={() => onToggleIoMethod(method)}
|
|
||||||
>
|
|
||||||
{isMethodExpanded ? (
|
|
||||||
<FaChevronDown className={styles.categoryIcon} />
|
|
||||||
) : (
|
|
||||||
<FaChevronRight className={styles.categoryIcon} />
|
|
||||||
)}
|
|
||||||
<span className={styles.categoryLabel}>{methodLabel}</span>
|
|
||||||
<span className={styles.categoryCount}>{nodes.length}</span>
|
|
||||||
</button>
|
|
||||||
{isMethodExpanded &&
|
|
||||||
nodes.map((node) => (
|
|
||||||
<NodeListItem
|
|
||||||
key={node.id}
|
|
||||||
node={node}
|
|
||||||
language={language}
|
|
||||||
getLabel={getLabelFn}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = groupedByCategory[catId] || [];
|
const items = groupedByCategory[catId] || [];
|
||||||
return (
|
return (
|
||||||
<div key={catId} className={styles.categoryGroup}>
|
<div key={catId} className={styles.categoryGroup}>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FaPlay, FaCodeBranch, FaDatabase, FaPlug, FaUser } from 'react-icons/fa';
|
import { FaPlay, FaCodeBranch, FaDatabase, FaPlug, FaUser, FaRobot, FaEnvelope, FaCloud } from 'react-icons/fa';
|
||||||
|
|
||||||
export const CATEGORY_ICONS: Record<string, React.ReactNode> = {
|
export const CATEGORY_ICONS: Record<string, React.ReactNode> = {
|
||||||
trigger: <FaPlay />,
|
trigger: <FaPlay />,
|
||||||
input: <FaUser />,
|
input: <FaUser />,
|
||||||
flow: <FaCodeBranch />,
|
flow: <FaCodeBranch />,
|
||||||
data: <FaDatabase />,
|
data: <FaDatabase />,
|
||||||
io: <FaPlug />,
|
ai: <FaRobot />,
|
||||||
|
email: <FaEnvelope />,
|
||||||
|
sharepoint: <FaCloud />,
|
||||||
human: <FaUser />,
|
human: <FaUser />,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* AI node config - prompt, query, document options per node type.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import type { NodeConfigRendererProps } from './types';
|
||||||
|
|
||||||
|
const AI_FIELD_CONFIG: Record<string, { label: string; key: string; type: 'textarea' | 'input' | 'select'; options?: string[] }[]> = {
|
||||||
|
'ai.prompt': [
|
||||||
|
{ label: 'Prompt', key: 'prompt', type: 'textarea' },
|
||||||
|
{ label: 'Output format', key: 'resultType', type: 'select', options: ['txt', 'json', 'md', 'html', 'csv'] },
|
||||||
|
],
|
||||||
|
'ai.webResearch': [{ label: 'Query', key: 'query', type: 'textarea' }],
|
||||||
|
'ai.summarizeDocument': [
|
||||||
|
{ label: 'Summary length', key: 'summaryLength', type: 'select', options: ['short', 'medium', 'long'] },
|
||||||
|
],
|
||||||
|
'ai.translateDocument': [{ label: 'Target language', key: 'targetLanguage', type: 'input' }],
|
||||||
|
'ai.convertDocument': [
|
||||||
|
{ label: 'Target format', key: 'targetFormat', type: 'select', options: ['pdf', 'docx', 'txt', 'md'] },
|
||||||
|
],
|
||||||
|
'ai.generateDocument': [
|
||||||
|
{ label: 'Prompt', key: 'prompt', type: 'textarea' },
|
||||||
|
{ label: 'Format', key: 'format', type: 'select', options: ['docx', 'txt', 'md'] },
|
||||||
|
],
|
||||||
|
'ai.generateCode': [
|
||||||
|
{ label: 'Prompt', key: 'prompt', type: 'textarea' },
|
||||||
|
{ label: 'Language', key: 'language', type: 'select', options: ['python', 'javascript', 'typescript', 'sql'] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AiNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam, nodeType = 'ai.prompt' }) => {
|
||||||
|
const fields = AI_FIELD_CONFIG[nodeType] ?? AI_FIELD_CONFIG['ai.prompt'];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{fields.map((f) => (
|
||||||
|
<div key={f.key}>
|
||||||
|
<label>{f.label}</label>
|
||||||
|
{f.type === 'textarea' ? (
|
||||||
|
<textarea
|
||||||
|
value={(params[f.key] as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam(f.key, e.target.value)}
|
||||||
|
placeholder={f.label}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
) : f.type === 'select' ? (
|
||||||
|
<select
|
||||||
|
value={(params[f.key] as string) ?? (f.options?.[0] ?? '')}
|
||||||
|
onChange={(e) => updateParam(f.key, e.target.value)}
|
||||||
|
>
|
||||||
|
{(f.options ?? []).map((opt) => (
|
||||||
|
<option key={opt} value={opt}>
|
||||||
|
{opt}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
value={(params[f.key] as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam(f.key, e.target.value)}
|
||||||
|
placeholder={f.label}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
240
src/components/Automation2FlowEditor/configs/EmailNodeConfig.tsx
Normal file
240
src/components/Automation2FlowEditor/configs/EmailNodeConfig.tsx
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
/**
|
||||||
|
* Email node config - connection selector, folder dropdown, query, subject, body.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import type { NodeConfigRendererProps } from './types';
|
||||||
|
import { fetchConnections, fetchBrowse, type UserConnection, type BrowseEntry } from '../../../api/automation2Api';
|
||||||
|
|
||||||
|
export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
||||||
|
params,
|
||||||
|
updateParam,
|
||||||
|
instanceId,
|
||||||
|
request,
|
||||||
|
nodeType = 'email.checkEmail',
|
||||||
|
}) => {
|
||||||
|
const [connections, setConnections] = useState<UserConnection[]>([]);
|
||||||
|
const [folders, setFolders] = useState<BrowseEntry[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [foldersLoading, setFoldersLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceId && request) {
|
||||||
|
setLoading(true);
|
||||||
|
fetchConnections(request, instanceId)
|
||||||
|
.then(setConnections)
|
||||||
|
.catch(() => setConnections([]))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}, [instanceId, request]);
|
||||||
|
|
||||||
|
const connectionId = (params.connectionId as string) ?? '';
|
||||||
|
const selectedConn = connections.find((c) => c.id === connectionId);
|
||||||
|
const mailService = selectedConn?.authority === 'google' ? 'gmail' : 'outlook';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceId && request && connectionId) {
|
||||||
|
setFoldersLoading(true);
|
||||||
|
fetchBrowse(request, instanceId, connectionId, mailService, '/')
|
||||||
|
.then((r) => setFolders(r.items.filter((e) => e.isFolder)))
|
||||||
|
.catch(() => setFolders([]))
|
||||||
|
.finally(() => setFoldersLoading(false));
|
||||||
|
} else {
|
||||||
|
setFolders([]);
|
||||||
|
}
|
||||||
|
}, [instanceId, request, connectionId, mailService]);
|
||||||
|
|
||||||
|
const isDraft = nodeType === 'email.draftEmail';
|
||||||
|
const isSearch = nodeType === 'email.searchEmail';
|
||||||
|
const folderValue = (params.folder as string) ?? (isSearch ? 'All' : 'Inbox');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label>Account</label>
|
||||||
|
<select
|
||||||
|
value={connectionId}
|
||||||
|
onChange={(e) => updateParam('connectionId', e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="">{loading ? 'Loading...' : 'Select connection'}</option>
|
||||||
|
{connections.map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.externalEmail ?? c.externalUsername ?? c.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{!isDraft && (
|
||||||
|
<div>
|
||||||
|
<label>Folder</label>
|
||||||
|
<select
|
||||||
|
value={folderValue}
|
||||||
|
onChange={(e) => updateParam('folder', e.target.value)}
|
||||||
|
disabled={foldersLoading || !connectionId}
|
||||||
|
>
|
||||||
|
<option value="">
|
||||||
|
{foldersLoading ? 'Loading folders...' : !connectionId ? 'Select account first' : 'Select folder'}
|
||||||
|
</option>
|
||||||
|
{isSearch && <option value="All">All</option>}
|
||||||
|
{folders.length > 0
|
||||||
|
? folders.map((f) => {
|
||||||
|
const folderId = (f.path ?? '').replace(/^\//, '') || (f.metadata as { id?: string })?.id || '';
|
||||||
|
const value = folderId || f.name;
|
||||||
|
if (!value) return null;
|
||||||
|
return (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{f.name}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: !isSearch && (
|
||||||
|
<>
|
||||||
|
<option value="Inbox">Inbox</option>
|
||||||
|
<option value="Drafts">Drafts</option>
|
||||||
|
<option value="SentItems">Sent Items</option>
|
||||||
|
<option value="DeletedItems">Deleted Items</option>
|
||||||
|
<option value="JunkEmail">Junk Email</option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{folderValue &&
|
||||||
|
!folders.some(
|
||||||
|
(f) =>
|
||||||
|
((f.path ?? '').replace(/^\//, '') || (f.metadata as { id?: string })?.id) === folderValue
|
||||||
|
) &&
|
||||||
|
folderValue !== 'All' && (
|
||||||
|
<option value={folderValue}>{folderValue}</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isSearch && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label>Search query (optional)</label>
|
||||||
|
<input
|
||||||
|
value={(params.query as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('query', e.target.value)}
|
||||||
|
placeholder="General search term (subject, body, from)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>From address (optional)</label>
|
||||||
|
<input
|
||||||
|
value={(params.fromAddress as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('fromAddress', e.target.value)}
|
||||||
|
placeholder="e.g. sender@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>To address (optional)</label>
|
||||||
|
<input
|
||||||
|
value={(params.toAddress as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('toAddress', e.target.value)}
|
||||||
|
placeholder="e.g. recipient@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Subject contains (optional)</label>
|
||||||
|
<input
|
||||||
|
value={(params.subjectContains as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('subjectContains', e.target.value)}
|
||||||
|
placeholder="Word or phrase in subject"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Body/content contains (optional)</label>
|
||||||
|
<input
|
||||||
|
value={(params.bodyContains as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('bodyContains', e.target.value)}
|
||||||
|
placeholder="Word or phrase in email body"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="searchHasAttachment"
|
||||||
|
checked={!!(params.hasAttachment as boolean)}
|
||||||
|
onChange={(e) => updateParam('hasAttachment', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="searchHasAttachment">Only emails with attachment</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Limit</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={(params.limit as number) ?? 100}
|
||||||
|
onChange={(e) => updateParam('limit', parseInt(e.target.value, 10) || 100)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{nodeType === 'email.checkEmail' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label>From address (optional)</label>
|
||||||
|
<input
|
||||||
|
value={(params.fromAddress as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('fromAddress', e.target.value)}
|
||||||
|
placeholder="e.g. sender@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Subject contains (optional)</label>
|
||||||
|
<input
|
||||||
|
value={(params.subjectContains as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('subjectContains', e.target.value)}
|
||||||
|
placeholder="Word or phrase in subject"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="hasAttachment"
|
||||||
|
checked={!!(params.hasAttachment as boolean)}
|
||||||
|
onChange={(e) => updateParam('hasAttachment', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<label htmlFor="hasAttachment">Only emails with attachment</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Limit</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={(params.limit as number) ?? 100}
|
||||||
|
onChange={(e) => updateParam('limit', parseInt(e.target.value, 10) || 100)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{isDraft && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label>Subject</label>
|
||||||
|
<input
|
||||||
|
value={(params.subject as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('subject', e.target.value)}
|
||||||
|
placeholder="Email subject (or leave empty if connected to AI node above)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Body</label>
|
||||||
|
<textarea
|
||||||
|
value={(params.body as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('body', e.target.value)}
|
||||||
|
placeholder="Email body (or leave empty if connected to AI node above)"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>To (optional)</label>
|
||||||
|
<input
|
||||||
|
value={(params.to as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('to', e.target.value)}
|
||||||
|
placeholder="Recipient(s) (or from AI when connected)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
/**
|
||||||
|
* SharePoint node config - connection selector, path, search query.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import type { NodeConfigRendererProps } from './types';
|
||||||
|
import { fetchConnections, fetchBrowse, type UserConnection, type BrowseEntry } from '../../../api/automation2Api';
|
||||||
|
|
||||||
|
export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
||||||
|
params,
|
||||||
|
updateParam,
|
||||||
|
instanceId,
|
||||||
|
request,
|
||||||
|
nodeType = 'sharepoint.findFile',
|
||||||
|
}) => {
|
||||||
|
const [connections, setConnections] = useState<UserConnection[]>([]);
|
||||||
|
const [browseItems, setBrowseItems] = useState<BrowseEntry[]>([]);
|
||||||
|
const [currentPath, setCurrentPath] = useState('/');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const connectionId = (params.connectionId as string) ?? '';
|
||||||
|
const path = (params.path as string) ?? '/';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceId && request) {
|
||||||
|
setLoading(true);
|
||||||
|
fetchConnections(request, instanceId)
|
||||||
|
.then(setConnections)
|
||||||
|
.catch(() => setConnections([]))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}
|
||||||
|
}, [instanceId, request]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (instanceId && request && connectionId) {
|
||||||
|
const service = 'sharepoint';
|
||||||
|
fetchBrowse(request, instanceId, connectionId, service, currentPath)
|
||||||
|
.then((r: { items: BrowseEntry[]; path: string; service: string }) => {
|
||||||
|
setBrowseItems(r.items);
|
||||||
|
setCurrentPath(r.path);
|
||||||
|
})
|
||||||
|
.catch(() => setBrowseItems([]));
|
||||||
|
} else {
|
||||||
|
setBrowseItems([]);
|
||||||
|
}
|
||||||
|
}, [instanceId, request, connectionId, currentPath]);
|
||||||
|
|
||||||
|
const navigateTo = (entryPath: string) => setCurrentPath(entryPath);
|
||||||
|
const selectPath = (p: string) => updateParam('path', p);
|
||||||
|
|
||||||
|
const needsPath = !['sharepoint.findFile'].includes(nodeType);
|
||||||
|
const needsSearch = nodeType === 'sharepoint.findFile';
|
||||||
|
const needsSiteId = ['sharepoint.uploadFile', 'sharepoint.downloadFile', 'sharepoint.copyFile'].includes(nodeType);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label>Connection</label>
|
||||||
|
<select
|
||||||
|
value={connectionId}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateParam('connectionId', e.target.value);
|
||||||
|
setCurrentPath('/');
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="">{loading ? 'Loading...' : 'Select connection'}</option>
|
||||||
|
{connections.map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.externalUsername ?? c.id}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{needsSearch && (
|
||||||
|
<div>
|
||||||
|
<label>Search query / path</label>
|
||||||
|
<input
|
||||||
|
value={(params.searchQuery as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('searchQuery', e.target.value)}
|
||||||
|
placeholder="/sites/SiteName/Shared Documents or search term"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{needsPath && nodeType === 'sharepoint.listFiles' && (
|
||||||
|
<div>
|
||||||
|
<label>Folder path</label>
|
||||||
|
<input
|
||||||
|
value={path}
|
||||||
|
onChange={(e) => updateParam('path', e.target.value)}
|
||||||
|
placeholder="/ or /sites/SiteName/Shared Documents/Folder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{needsPath && ['sharepoint.readFile', 'sharepoint.uploadFile', 'sharepoint.downloadFile'].includes(nodeType) && (
|
||||||
|
<div>
|
||||||
|
<label>Path</label>
|
||||||
|
<input
|
||||||
|
value={(params.path as string) ?? (params.filePath as string) ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateParam(nodeType === 'sharepoint.downloadFile' ? 'filePath' : 'path', e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="File or folder path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{needsSiteId && (
|
||||||
|
<div>
|
||||||
|
<label>Site ID</label>
|
||||||
|
<input
|
||||||
|
value={(params.siteId as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('siteId', e.target.value)}
|
||||||
|
placeholder="SharePoint site ID"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{nodeType === 'sharepoint.uploadFile' && (
|
||||||
|
<div>
|
||||||
|
<label>File name</label>
|
||||||
|
<input
|
||||||
|
value={(params.fileName as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('fileName', e.target.value)}
|
||||||
|
placeholder="file.pdf"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{nodeType === 'sharepoint.copyFile' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label>Source folder</label>
|
||||||
|
<input
|
||||||
|
value={(params.sourceFolder as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('sourceFolder', e.target.value)}
|
||||||
|
placeholder="Source folder path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Source file</label>
|
||||||
|
<input
|
||||||
|
value={(params.sourceFile as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('sourceFile', e.target.value)}
|
||||||
|
placeholder="Source file name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Dest folder</label>
|
||||||
|
<input
|
||||||
|
value={(params.destFolder as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('destFolder', e.target.value)}
|
||||||
|
placeholder="Destination folder path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Dest file</label>
|
||||||
|
<input
|
||||||
|
value={(params.destFile as string) ?? ''}
|
||||||
|
onChange={(e) => updateParam('destFile', e.target.value)}
|
||||||
|
placeholder="Destination file name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{connectionId && needsPath && (
|
||||||
|
<div className="browse-section" style={{ marginTop: 12 }}>
|
||||||
|
<label>Browse: {currentPath}</label>
|
||||||
|
<ul style={{ maxHeight: 150, overflow: 'auto', listStyle: 'none', paddingLeft: 0 }}>
|
||||||
|
{currentPath !== '/' && (
|
||||||
|
<li>
|
||||||
|
<button type="button" onClick={() => navigateTo(currentPath.replace(/\/[^/]+$/, '') || '/')}>
|
||||||
|
.. (parent)
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{browseItems.map((e) => (
|
||||||
|
<li key={e.path}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => (e.isFolder ? navigateTo(e.path) : selectPath(e.path))}
|
||||||
|
title={e.path}
|
||||||
|
>
|
||||||
|
{e.isFolder ? '📁' : '📄'} {e.name}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Node config renderers - one per input node type.
|
* Node config renderers - one per node type (input, ai, email, sharepoint).
|
||||||
* Add new node types here.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
|
|
@ -12,6 +11,9 @@ import { CommentNodeConfig } from './CommentNodeConfig';
|
||||||
import { ReviewNodeConfig } from './ReviewNodeConfig';
|
import { ReviewNodeConfig } from './ReviewNodeConfig';
|
||||||
import { SelectionNodeConfig } from './SelectionNodeConfig';
|
import { SelectionNodeConfig } from './SelectionNodeConfig';
|
||||||
import { ConfirmationNodeConfig } from './ConfirmationNodeConfig';
|
import { ConfirmationNodeConfig } from './ConfirmationNodeConfig';
|
||||||
|
import { AiNodeConfig } from './AiNodeConfig';
|
||||||
|
import { EmailNodeConfig } from './EmailNodeConfig';
|
||||||
|
import { SharePointNodeConfig } from './SharePointNodeConfig';
|
||||||
|
|
||||||
export type NodeConfigComponent = ComponentType<NodeConfigRendererProps>;
|
export type NodeConfigComponent = ComponentType<NodeConfigRendererProps>;
|
||||||
|
|
||||||
|
|
@ -23,4 +25,20 @@ export const NODE_CONFIG_REGISTRY: Record<string, NodeConfigComponent> = {
|
||||||
'input.review': ReviewNodeConfig,
|
'input.review': ReviewNodeConfig,
|
||||||
'input.selection': SelectionNodeConfig,
|
'input.selection': SelectionNodeConfig,
|
||||||
'input.confirmation': ConfirmationNodeConfig,
|
'input.confirmation': ConfirmationNodeConfig,
|
||||||
|
'ai.prompt': AiNodeConfig,
|
||||||
|
'ai.webResearch': AiNodeConfig,
|
||||||
|
'ai.summarizeDocument': AiNodeConfig,
|
||||||
|
'ai.translateDocument': AiNodeConfig,
|
||||||
|
'ai.convertDocument': AiNodeConfig,
|
||||||
|
'ai.generateDocument': AiNodeConfig,
|
||||||
|
'ai.generateCode': AiNodeConfig,
|
||||||
|
'email.checkEmail': EmailNodeConfig,
|
||||||
|
'email.searchEmail': EmailNodeConfig,
|
||||||
|
'email.draftEmail': EmailNodeConfig,
|
||||||
|
'sharepoint.findFile': SharePointNodeConfig,
|
||||||
|
'sharepoint.readFile': SharePointNodeConfig,
|
||||||
|
'sharepoint.uploadFile': SharePointNodeConfig,
|
||||||
|
'sharepoint.listFiles': SharePointNodeConfig,
|
||||||
|
'sharepoint.downloadFile': SharePointNodeConfig,
|
||||||
|
'sharepoint.copyFile': SharePointNodeConfig,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,15 @@
|
||||||
* Shared types for node config renderers
|
* Shared types for node config renderers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ApiRequestFunction } from '../../../api/automation2Api';
|
||||||
|
|
||||||
export type FormField = { name?: string; type?: string; label?: string; required?: boolean };
|
export type FormField = { name?: string; type?: string; label?: string; required?: boolean };
|
||||||
|
|
||||||
export interface NodeConfigRendererProps {
|
export interface NodeConfigRendererProps {
|
||||||
params: Record<string, unknown>;
|
params: Record<string, unknown>;
|
||||||
updateParam: (key: string, value: unknown) => void;
|
updateParam: (key: string, value: unknown) => void;
|
||||||
|
/** For Email/SharePoint: fetch connections and browse */
|
||||||
|
instanceId?: string;
|
||||||
|
request?: ApiRequestFunction;
|
||||||
|
nodeType?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,15 @@
|
||||||
/**
|
/**
|
||||||
* Automation2 Flow Editor - Constants
|
* Automation2 Flow Editor - Constants
|
||||||
* I/O method configuration, category ordering.
|
* Category ordering for node sidebar.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** I/O nodes: order for sub-groups (KI, Kontext, Outlook, etc.) */
|
|
||||||
export const IO_METHOD_ORDER = [
|
|
||||||
'ai',
|
|
||||||
'context',
|
|
||||||
'outlook',
|
|
||||||
'sharepoint',
|
|
||||||
'jira',
|
|
||||||
'trustee',
|
|
||||||
'chatbot',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const IO_METHOD_LABELS: Record<string, Record<string, string>> = {
|
|
||||||
ai: { de: 'KI', en: 'AI', fr: 'IA' },
|
|
||||||
context: { de: 'Kontext', en: 'Context', fr: 'Contexte' },
|
|
||||||
outlook: { de: 'Outlook', en: 'Outlook', fr: 'Outlook' },
|
|
||||||
sharepoint: { de: 'SharePoint', en: 'SharePoint', fr: 'SharePoint' },
|
|
||||||
jira: { de: 'Jira', en: 'Jira', fr: 'Jira' },
|
|
||||||
trustee: { de: 'Trustee', en: 'Trustee', fr: 'Trustee' },
|
|
||||||
chatbot: { de: 'Chatbot', en: 'Chatbot', fr: 'Chatbot' },
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Default category display order */
|
/** Default category display order */
|
||||||
export const CATEGORY_ORDER = ['trigger', 'input', 'flow', 'data', 'io'] as const;
|
export const CATEGORY_ORDER = [
|
||||||
|
'trigger',
|
||||||
|
'input',
|
||||||
|
'flow',
|
||||||
|
'data',
|
||||||
|
'ai',
|
||||||
|
'email',
|
||||||
|
'sharepoint',
|
||||||
|
] as const;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { CATEGORY_ICONS, DEFAULT_CATEGORY_ICON } from './categoryIcons';
|
import { CATEGORY_ICONS, DEFAULT_CATEGORY_ICON } from './categoryIcons';
|
||||||
import { IO_METHOD_LABELS } from './constants';
|
|
||||||
|
|
||||||
/** Resolve localized label from string or { de, en, fr } object */
|
/** Resolve localized label from string or { de, en, fr } object */
|
||||||
export function getLabel(
|
export function getLabel(
|
||||||
|
|
@ -22,10 +21,5 @@ export function getCategoryIcon(categoryId: string): React.ReactNode {
|
||||||
return CATEGORY_ICONS[categoryId] ?? DEFAULT_CATEGORY_ICON;
|
return CATEGORY_ICONS[categoryId] ?? DEFAULT_CATEGORY_ICON;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get label for I/O method (ai, context, outlook, ...) */
|
|
||||||
export function getIoMethodLabel(method: string, lang: string): string {
|
|
||||||
return IO_METHOD_LABELS[method]?.[lang] ?? IO_METHOD_LABELS[method]?.en ?? method;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Function type for resolving localized labels */
|
/** Function type for resolving localized labels */
|
||||||
export type GetLabelFn = (text: string | Record<string, string> | undefined, lang?: string) => string;
|
export type GetLabelFn = (text: string | Record<string, string> | undefined, lang?: string) => string;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue