mirror of
https://github.com/reconurge/flowsint.git
synced 2026-07-19 18:00:42 -05:00
feat: context menu on nodes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<string, { table: string; type: string; fields: string[] }> = {}
|
||||
|
||||
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<string, any>) => {
|
||||
|
||||
let result: Record<string, any> = {};
|
||||
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<T extends FlattenableNode>(nodes: T[], key: string): Omit<T, typeof key>[] {
|
||||
let result: Omit<T, typeof key>[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
const { [key]: children, ...rest } = node;
|
||||
result.push(rest as Omit<T, typeof key>);
|
||||
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})`
|
||||
}
|
||||
Reference in New Issue
Block a user