From 6cdc01050ba66dedd613691880bbe9dee208cded Mon Sep 17 00:00:00 2001 From: dextmorgn Date: Fri, 20 Jun 2025 11:37:21 +0200 Subject: [PATCH] feat: context menu on nodes --- .../src/renderer/src/lib/action-items.ts | 640 ++++++++++++++++++ .../src/renderer/src/lib/auth-utils.ts | 31 + flowsint-app/src/renderer/src/lib/utils.ts | 364 ++++++++++ 3 files changed, 1035 insertions(+) create mode 100644 flowsint-app/src/renderer/src/lib/action-items.ts create mode 100644 flowsint-app/src/renderer/src/lib/auth-utils.ts create mode 100644 flowsint-app/src/renderer/src/lib/utils.ts diff --git a/flowsint-app/src/renderer/src/lib/action-items.ts b/flowsint-app/src/renderer/src/lib/action-items.ts new file mode 100644 index 00000000..a606e8a2 --- /dev/null +++ b/flowsint-app/src/renderer/src/lib/action-items.ts @@ -0,0 +1,640 @@ +import { + AtSign, + Bike, + Bitcoin, + Building2, + Car, + Facebook, + Ghost, + Github, + Globe, + Instagram, + Linkedin, + Locate, + MapPin, + MessageCircle, + MessageSquare, + Phone, + Plane, + Sailboat, + Send, + Twitch, + Twitter, + User, + UserCheckIcon, + Video, + type LucideIcon, +} from "lucide-react" + + +export type FieldType = "text" | "date" | "email" | "number" | "select" | "textarea" | "hidden" | "tel" | "url" | "metadata" + +export interface SelectOption { + label: string + value: string +} + +export interface FormField { + name: string + label: string + type: FieldType + required: boolean + options?: SelectOption[] + placeholder?: string + description?: string +} +export interface ActionItem { + id: number + type: string + table: string + key: string + icon: LucideIcon + label: string + color?: string + fields: FormField[] + size?: string + label_key: string + disabled?: boolean + comingSoon?: boolean + children?: ActionItem[] +} + +const countries = [ + { label: "France", value: "france" }, + { label: "United States", value: "usa" }, + { label: "United Kingdom", value: "uk" }, + { label: "Germany", value: "germany" }, + { label: "Japan", value: "japan" }, + { label: "Canada", value: "canada" }, + { label: "Australia", value: "australia" }, +] +export const actionItems: ActionItem[] = [ + { + id: 100, + type: "person", + table: "", + key: "person", + icon: User, + label: "Person", + label_key: "name", + fields: [], + children: [ + { + id: 1, + type: "individual", + table: "individuals", + key: "individual", + label_key: "first_name", + icon: User, + label: "Individual", + fields: [{ name: "first_name", label: "Firstname", type: "text", required: true }, + { name: "last_name", label: "Lastname", type: "text", required: true }, + { name: "birth_date", label: "Birth date", type: "date", required: false }, + { + name: "gender", + label: "Gender", + type: "select", + required: false, + options: [ + { label: "Male", value: "male" }, + { label: "Female", value: "female" }, + { label: "Non-binary", value: "non-binary" }, + { label: "Prefer not to say", value: "not_specified" }, + ], + }, + ], + size: "h-7 w-7", + }, + { + id: 512932, + type: "username", + table: "usernames", + key: "username", + icon: UserCheckIcon, + label: "Username", + label_key: "username", + fields: [{ name: "username", label: "Username", type: "text", required: true }], + size: "h-5 w-5", + }, + ], + }, + { + id: 101, + type: "organization", + table: "", + key: "organization_category", + icon: Building2, + label: "Organization", + label_key: "name", + fields: [], + children: [ + { + id: 24, + type: "organization", + table: "organizations", + key: "organization", + icon: Building2, + label: "Organization", + label_key: "name", + fields: [ + { name: "name", label: "Name", type: "text", required: true }, + { name: "founding_date", label: "Founding date", type: "date", required: false }, + { + name: "country", + label: "Country", + type: "select", + required: false, + options: countries, + }, + { + name: "metadata", + label: "Metadata", + type: "metadata", + required: false, + description: "Add additional information under the format key:value" + } + ], + size: "h-8 w-8", + }, + ], + }, + { + id: 102, + type: "contact", + table: "", + key: "contact", + icon: Phone, + label: "Contact", + label_key: "value", + fields: [], + children: [ + { + id: 2, + type: "phone", + table: "phones", + key: "phone", + label_key: "number", + icon: Phone, + label: "Phone number", + fields: [{ name: "number", label: "Phone number", type: "tel", required: true }, + { + name: "country", + label: "Country", + type: "select", + required: false, + options: countries, + }, + { name: "carrier", label: "Carrier", type: "text", required: false } + ], + size: "h-5 w-5", + }, + { + id: 4, + type: "email", + table: "emails", + key: "email", + icon: AtSign, + label_key: "email", + label: "Email address", + fields: [{ name: "email", label: "Email", type: "email", required: true }], + size: "h-5 w-5", + }, + ], + }, + { + id: 103, + type: "digital", + table: "", + key: "digital", + icon: Globe, + label: "Digital", + label_key: "value", + fields: [], + children: [ + { + id: 25, + type: "website", + table: "websites", + key: "website", + label_key: "url", + icon: Globe, + label: "Website", + fields: [ + { name: "url", label: "URL", type: "url", required: true }, + { name: "registration_date", label: "Registration date", type: "date", required: false }, + { name: "registrar", label: "Registrar", type: "text", required: false }, + { name: "address", label: "IP address", type: "text", required: false }, + ], + size: "h-5 w-5", + }, + { + id: 259, + type: "domain", + table: "domains", + key: "domain", + icon: Locate, + label_key: "domain", + label: "Domain", + fields: [{ name: "domain", label: "Domain", type: "text", required: true }], + size: "h-5 w-5", + }, + { + id: 2588, + type: "subdomain", + table: "subdomains", + key: "subdomain", + label_key: "subomain", + icon: Locate, + label: "Subdomain", + fields: [{ name: "subomain", label: "subdomain", type: "text", required: true }, { name: "root", label: "root domain", type: "text", required: true }], + size: "h-5 w-5", + }, + { + id: 5, + type: "ip", + table: "ips", + key: "ip", + icon: Locate, + label: "IP address", + label_key: "address", + fields: [{ name: "address", label: "IP address", type: "text", required: true }], + size: "h-5 w-5", + }, + ], + }, + { + id: 104, + type: "finance", + table: "", + key: "finance", + icon: Bitcoin, + label: "Finance", + label_key: "value", + fields: [], + children: [ + { + id: 215, + type: "wallet", + table: "wallets", + key: "wallet", + label_key: "wallet", + icon: Bitcoin, + label: "Crypto wallet", + fields: [ + { name: "wallet", label: "Wallet", type: "text", required: true }, + { + name: "type", + label: "Wallet type", + type: "select", + required: true, + options: [ + { label: "ETH", value: "eth" }, + { label: "BTC", value: "btc" }, + { label: "TRC", value: "trc" },], + }, + ], + size: "h-5 w-5", + }, + ], + }, + { + id: 105, + type: "location", + table: "", + key: "location", + icon: MapPin, + label: "Location", + label_key: "address", + fields: [], + children: [ + { + id: 3, + type: "address", + table: "addresses", + label_key: "address", + key: "address", + icon: MapPin, + label: "Physical address", + fields: [ + { name: "address", label: "Address", type: "text", required: true }, + { name: "city", label: "City", type: "text", required: true }, + { name: "country", label: "Country", type: "text", required: true }, + { name: "zip", label: "ZIP/Postal code", type: "text", required: false }, + { name: "latitude", label: "Latitude", type: "text", required: false }, + { name: "longitude", label: "Longitude", type: "text", required: false }, + ], + size: "h-5 w-5", + }, + ], + }, + { + id: 6, + type: "social_profile", + size: "h-5 w-5", + table: "", + key: "social_profile", + icon: Send, + label: "Social account", + label_key: "username", + fields: [], + children: [ + { + id: 7, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_facebook", + label_key: "username", + icon: Facebook, + color: "#1d4ed8", + label: "Facebook", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 8, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_instagram", + label_key: "username", + icon: Instagram, + color: "#db2777", + label: "Instagram", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 9, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_telegram", + label_key: "username", + icon: Send, + color: "#0369a1", + label: "Telegram", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 10, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_signal", + label_key: "username", + icon: MessageCircle, + color: "#3B45FC", + label: "Signal", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 11, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_snapchat", + label_key: "username", + color: "#FEFC00", + icon: Ghost, + label: "Snapchat", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 12, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_github", + label_key: "username", + icon: Github, + label: "Github", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: false }, + { name: "username", label: "Username", type: "text", required: true }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 13, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_coco", + label_key: "username", + icon: Send, + label: "Coco", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + disabled: true, + comingSoon: true, + size: "h-5 w-5", + }, + { + id: 18, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_linkedin", + label_key: "username", + color: "#116AC9", + icon: Linkedin, + label: "LinkedIn", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 19, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_twitter", + label_key: "username", + icon: Twitter, + label: "Twitter", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 20, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_tiktok", + label_key: "username", + icon: Video, + label: "TikTok", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 21, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_reddit", + label_key: "username", + icon: MessageSquare, + color: "#FF4B13", + label: "Reddit", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 22, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_discord", + label_key: "username", + icon: MessageCircle, + color: "#525FEE", + label: "Discord", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 2343, + type: "social_profile", + table: "social_profiles", + key: "social_profiles_twitch", + label_key: "username", + icon: Twitch, + color: "#A96FFF", + label: "Twitch", + fields: [ + { name: "profile_url", label: "Profile URL", type: "url", required: true }, + { name: "username", label: "Username", type: "text", required: false }, + { name: "platform", label: "Platform", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + ], + }, + { + id: 14, + type: "vehicle", + table: "", + icon: Car, + label: "Vehicle", + label_key: "plate", + key: "vehicle", + fields: [], + children: [ + { + id: 15, + type: "vehicle", + table: "vehicles", + key: "vehicles_car", + icon: Car, + label: "Car", + label_key: "plate", + fields: [ + { name: "plate", label: "License plate", type: "text", required: true }, + { name: "model", label: "Model", type: "text", required: false }, + { name: "brand", label: "Brand", type: "text", required: false }, + { name: "year", label: "Year", type: "number", required: false }, + { name: "type", label: "Type", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 16, + type: "vehicle", + table: "vehicles", + key: "vehicles_motorcycle", + label_key: "plate", + icon: Bike, + label: "Motorcycle", + fields: [ + { name: "plate", label: "License plate", type: "text", required: true }, + { name: "model", label: "Model", type: "text", required: false }, + { name: "brand", label: "Brand", type: "text", required: false }, + { name: "year", label: "Year", type: "number", required: false }, + { name: "type", label: "Type", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 17, + type: "vehicle", + table: "vehicles", + key: "vehicles_boat", + label_key: "plate", + icon: Sailboat, + label: "Boat", + fields: [ + { name: "plate", label: "Registration number", type: "text", required: true }, + { name: "model", label: "Model", type: "text", required: false }, + { name: "brand", label: "Brand", type: "text", required: false }, + { name: "year", label: "Year", type: "number", required: false }, + { name: "type", label: "Type", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + { + id: 23, + type: "vehicle", + table: "vehicles", + key: "vehicles_aircraft", + icon: Plane, + label_key: "plate", + label: "Aircraft", + fields: [ + { name: "registration", label: "Registration", type: "text", required: true }, + { name: "model", label: "Model", type: "text", required: false }, + { name: "manufacturer", label: "Manufacturer", type: "text", required: false }, + { name: "year", label: "Year", type: "number", required: false }, + { name: "type", label: "Type", type: "hidden", required: true }, + ], + size: "h-5 w-5", + }, + ], + }, +] + +export function findActionItemByKey(key: string): ActionItem | undefined { + for (const item of actionItems) { + if (item.key === key) return item + if (item.children) { + const found = item.children.find(child => child.key === key) + if (found) return found + } + } + return undefined; +} \ No newline at end of file diff --git a/flowsint-app/src/renderer/src/lib/auth-utils.ts b/flowsint-app/src/renderer/src/lib/auth-utils.ts new file mode 100644 index 00000000..6d6b704f --- /dev/null +++ b/flowsint-app/src/renderer/src/lib/auth-utils.ts @@ -0,0 +1,31 @@ +import { useAuthStore } from '@/stores/auth-store' +import { redirect } from '@tanstack/react-router' +import type { User } from '@/stores/auth-store' + +export function requireAuth(locationHref: string) { + const isAuth = useAuthStore.getState().isAuthenticated + const token = useAuthStore.getState().token + + if (!isAuth || !token) { + throw redirect({ + to: '/login', + search: { + redirect: locationHref, + }, + }) + } +} + +export function isAuthenticated(): boolean { + const isAuth = useAuthStore.getState().isAuthenticated + const token = useAuthStore.getState().token + return Boolean(isAuth && token) +} + +export function getAuthToken(): string | null { + return useAuthStore.getState().token +} + +export function getUser(): User | null { + return useAuthStore.getState().user +} diff --git a/flowsint-app/src/renderer/src/lib/utils.ts b/flowsint-app/src/renderer/src/lib/utils.ts new file mode 100644 index 00000000..64662648 --- /dev/null +++ b/flowsint-app/src/renderer/src/lib/utils.ts @@ -0,0 +1,364 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" +import Dagre from '@dagrejs/dagre'; +import { actionItems } from "./action-items" +import type { ActionItem, FormField } from "./action-items" +import { type Edge, Position, type Node } from '@xyflow/react'; +import * as d3 from "d3-force" + +interface NodePosition { + x: number; + y: number; +} + +interface NodeMeasured { + width: number; + height: number; +} + +interface NodeInternals { + positionAbsolute: NodePosition; +} + +interface FlowNode { + measured: NodeMeasured; + internals: NodeInternals; +} + +interface IntersectionPoint { + x: number; + y: number; +} + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export const zoomSelector = (s: { transform: number[]; }) => s.transform[2] >= 0.6; + + +// this helper function returns the intersection point +// of the line between the center of the intersectionNode and the target node +function getNodeIntersection( + intersectionNode: FlowNode, + targetNode: FlowNode +): IntersectionPoint { + const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured; + const intersectionNodePosition = intersectionNode.internals.positionAbsolute; + const targetPosition = targetNode.internals.positionAbsolute; + + const w = intersectionNodeWidth / 2; + const h = intersectionNodeHeight / 2; + + const x2 = intersectionNodePosition.x + w; + const y2 = intersectionNodePosition.y + h; + const x1 = targetPosition.x + targetNode.measured.width / 2; + const y1 = targetPosition.y + targetNode.measured.height / 2; + + const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h); + const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h); + const a = 1 / (Math.abs(xx1) + Math.abs(yy1)); + const xx3 = a * xx1; + const yy3 = a * yy1; + const x = w * (xx3 + yy3) + x2; + const y = h * (-xx3 + yy3) + y2; + + return { x, y }; +} + +// returns the position (top,right,bottom or right) passed node compared to the intersection point +function getEdgePosition(node: FlowNode, intersectionPoint: IntersectionPoint): Position { + const n = { ...node.internals.positionAbsolute, ...node }; + const nx = Math.round(n.x); + const ny = Math.round(n.y); + const px = Math.round(intersectionPoint.x); + const py = Math.round(intersectionPoint.y); + + if (px <= nx + 1) { + return Position.Left; + } + if (px >= nx + n.measured.width - 1) { + return Position.Right; + } + if (py <= ny + 1) { + return Position.Top; + } + if (py >= n.y + n.measured.height - 1) { + return Position.Bottom; + } + + return Position.Top; +} + +// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge +interface EdgeParams { + sx: number; + sy: number; + tx: number; + ty: number; + sourcePos: Position; + targetPos: Position; +} + +export function getEdgeParams(source: FlowNode, target: FlowNode): EdgeParams { + const sourceIntersectionPoint = getNodeIntersection(source, target); + const targetIntersectionPoint = getNodeIntersection(target, source); + + const sourcePos = getEdgePosition(source, sourceIntersectionPoint); + const targetPos = getEdgePosition(target, targetIntersectionPoint); + + return { + sx: sourceIntersectionPoint.x, + sy: sourceIntersectionPoint.y, + tx: targetIntersectionPoint.x, + ty: targetIntersectionPoint.y, + sourcePos, + targetPos, + }; +} + +interface LayoutOptions { + direction?: "LR" | "TB"; + strength?: number; + distance?: number; + iterations?: number; +} + +export const getForceLayoutedElements = ( + nodes: Node[], + edges: Edge[], + options: LayoutOptions = { + direction: "LR", + strength: -300, + distance: 100, + iterations: 300, + }, +) => { + // Create a map of node IDs to indices for the simulation + const nodeMap = new Map(nodes.map((node, i) => [node.id, i])) + + // Create a copy of nodes with positions for the simulation + const nodesCopy = nodes.map((node) => ({ + ...node, + x: node.position?.x || Math.random() * 500, + y: node.position?.y || Math.random() * 500, + width: node.measured?.width || 0, + height: node.measured?.height || 0, + })) + + // Create links for the simulation using indices + const links = edges.map((edge) => ({ + source: nodeMap.get(edge.source), + target: nodeMap.get(edge.target), + original: edge, + })) + + // Create the simulation + const simulation = d3 + .forceSimulation(nodesCopy) + .force( + "link", + d3.forceLink(links).id((d: any) => nodeMap.get(d.id)), + ) + .force("charge", d3.forceManyBody().strength(options.strength || -300)) + .force("center", d3.forceCenter(250, 250)) + .force( + "collision", + d3.forceCollide().radius((d: any) => Math.max(d.width, d.height) / 2 + 10), + ) + + // If direction is horizontal, adjust forces + if (options.direction === "LR") { + simulation.force("x", d3.forceX(250).strength(0.1)) + simulation.force("y", d3.forceY(250).strength(0.05)) + } else { + simulation.force("x", d3.forceX(250).strength(0.05)) + simulation.force("y", d3.forceY(250).strength(0.1)) + } + + // Run the simulation synchronously + simulation.stop() + for (let i = 0; i < (options.iterations || 300); i++) { + simulation.tick() + } + + // Update node positions based on simulation results + const updatedNodes = nodesCopy.map((node) => ({ + ...node, + position: { + x: node.x - node.width / 2, + y: node.y - node.height / 2, + }, + })) + + return { + nodes: updatedNodes, + edges, + } +} + +export const getDagreLayoutedElements = (nodes: Node[], + edges: Edge[], + options: LayoutOptions = { + direction: "TB", + strength: -300, + distance: 100, + iterations: 300, + },) => { + + const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + g.setGraph({ rankdir: options.direction }); + + edges.forEach((edge) => g.setEdge(edge.source, edge.target)); + nodes.forEach((node) => + g.setNode(node.id, { + ...node, + width: node.measured?.width ?? 0, + height: node.measured?.height ?? 0, + }), + ); + + Dagre.layout(g); + + return { + nodes: nodes.map((node) => { + const position = g.node(node.id); + // We are shifting the dagre node position (anchor=center center) to the top left + // so it matches the React Flow node anchor point (top left). + const x = position.x - (node.measured?.width ?? 0) / 2; + const y = position.y - (node.measured?.height ?? 0) / 2; + + return { ...node, position: { x, y } }; + }), + edges, + }; +}; + + +export const sanitize = (name: string) => { + return name + .normalize("NFKD") // Decompose special characters (e.g., accents) + .replace(/[\u0300-\u036f]/g, "") // Remove accent marks + .replace(/[^a-zA-Z0-9.\-_]/g, "_") // Replace invalid characters with underscores + .replace(/_{2,}/g, "_") // Replace multiple underscores with a single one + .replace(/^_|_$/g, "") // Trim leading or trailing underscores + .toLowerCase(); // Convert to lowercase for consistency +}; + +export const formatFileSize = (bytes: number) => { + if (bytes < 1024) return bytes + " B" + else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB" + else return (bytes / 1048576).toFixed(1) + " MB" +} + + +function convertFieldToSimpleFormat(field: FormField): string { + if ((field.type === "hidden" && field.name === "platform") || field.name === "type") { + const defaultValue = field.options?.[0]?.value || "" + if (defaultValue) { + return `${field.name}:${defaultValue}` + } + } + return field.name +} + +export function generateNodeTypes() { + const nodeTypes: Record = {} + + function processItems(items: ActionItem[]) { + items.forEach((item) => { + if (item.table && !item.disabled) { + nodeTypes[item.key] = { + table: item.table, + type: item.type, + fields: item.fields.map(convertFieldToSimpleFormat), + } + } + + // Traiter les enfants récursivement + if (item.children && item.children.length > 0) { + processItems(item.children) + } + }) + } + + processItems(actionItems) + return nodeTypes +} + +export const nodesTypes = generateNodeTypes() + +export const getInitials = (name: string) => { + return name + .split(" ") + .map((part) => part[0]) + .join("") + .toUpperCase() + .substring(0, 2) +} + + +export function getAvatarColor(name: string): string { + const colors = [ + "bg-red-500", + "bg-blue-500", + "bg-green-500", + "bg-yellow-500", + "bg-purple-500", + "bg-pink-500", + "bg-indigo-500", + "bg-teal-500", + "bg-orange-500", + "bg-cyan-500", + ] + let hash = 0 + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash) + } + const index = Math.abs(hash) % colors.length + return colors[index] +} + + +export const flattenObj = (ob: Record) => { + + let result: Record = {}; + for (const i in ob) { + if (ob[i] && typeof ob[i] === 'object' && !Array.isArray(ob[i])) { + const temp = flattenObj(ob[i]); + for (const j in temp) { + result[i + '.' + j] = temp[j]; + } + } + else { + result[i] = ob[i]; + } + } + return result; +} + +interface FlattenableNode { + [key: string]: unknown; + children?: FlattenableNode[]; +} + +export function flattenArray(nodes: T[], key: string): Omit[] { + let result: Omit[] = []; + + for (const node of nodes) { + const { [key]: children, ...rest } = node; + result.push(rest as Omit); + if (children) { + result = result.concat(flattenArray(children as T[], key)); + } + } + return result; +} + + +export function hexToRgba(hex: string, alpha: number) { + const r = parseInt(hex.slice(1, 3), 16) + const g = parseInt(hex.slice(3, 5), 16) + const b = parseInt(hex.slice(5, 7), 16) + return `rgba(${r}, ${g}, ${b}, ${alpha})` +} \ No newline at end of file