sozsoft-platform/ui/src/components/codeLayout/PropertyPanel.tsx
Sedat ÖZTÜRK 524a88274b Notification UiToast, UiActivity, Desktop düzenlemesi
Fazla Console.Log kaldırıldı.
2026-05-11 15:19:27 +03:00

631 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from "react";
import TailwindModal from "./TailwindModal";
import { ComponentInfo, HookInfo, PropertyInfo } from "../../proxy/developerKit/componentInfo";
import { getComponentDefinition } from "./data/componentDefinitions";
import { Button } from "../ui";
interface PropertyPanelProps {
selectedComponent: ComponentInfo | null;
currentCode: string;
onPropertiesChange: (
componentId: string,
updates: Record<string, any>
) => void;
onHookToggle: (
componentId: string,
hookType: string,
enabled: boolean
) => void;
onMultipleHookToggle: (
toggles: { componentId: string; hookType: string; enabled: boolean }[]
) => void;
onDeleteComponent: (componentId: string) => void;
}
const PropertyPanel: React.FC<PropertyPanelProps> = ({
selectedComponent,
currentCode,
onPropertiesChange,
onHookToggle,
onMultipleHookToggle,
onDeleteComponent,
}) => {
const [tailwindModalOpen, setTailwindModalOpen] = useState(false);
const [currentTailwindProperty, setCurrentTailwindProperty] =
useState<string>("");
const [activeHooks, setActiveHooks] = useState<Set<string>>(new Set());
const [activeTab, setActiveTab] = useState<"props" | "hooks">("props");
// Local state for pending changes
const [pendingProperties, setPendingProperties] = useState<
Record<string, any>
>({});
const [pendingEvents, setPendingEvents] = useState<Record<string, string>>(
{}
);
const [pendingHooks, setPendingHooks] = useState<Record<string, boolean>>({});
const [hasChanges, setHasChanges] = useState(false);
const [hasHookChanges, setHasHookChanges] = useState(false);
const componentDefinition = selectedComponent
? getComponentDefinition(selectedComponent.name)
: null;
// Reset pending changes when component changes
useEffect(() => {
setPendingProperties({});
setPendingEvents({});
setPendingHooks({});
setHasChanges(false);
setHasHookChanges(false);
}, [selectedComponent?.id]);
// Check which hooks are currently active in the code
useEffect(() => {
if (selectedComponent && currentCode) {
const hooks = new Set<string>();
// Check for useState
if (
currentCode.includes(
`const [state_${selectedComponent.id}, setState_${selectedComponent.id}]`
)
) {
hooks.add("useState");
}
// Check for useRef
if (
currentCode.includes(`const ref_${selectedComponent.id}`) &&
currentCode.includes(`ref={ref_${selectedComponent.id}`)
) {
hooks.add("useRef");
}
// // Check for useEffect
// if (
// currentCode.includes("useEffect") &&
// currentCode.includes(selectedComponent.id)
// ) {
// hooks.add("useEffect");
// }
setActiveHooks(hooks);
}
}, [selectedComponent, currentCode]);
// Handle local property changes
const handleLocalPropertyChange = (propName: string, value: any) => {
setPendingProperties((prev) => ({
...prev,
[propName]: value,
}));
setHasChanges(true);
};
// Handle local hook changes
const handleLocalHookToggle = (hookType: string, enabled: boolean) => {
setPendingHooks((prev) => ({
...prev,
[hookType]: enabled,
}));
setHasHookChanges(true);
};
// Handle local event changes
const handleLocalEventChange = (eventName: string, value: string) => {
setPendingEvents((prev) => ({
...prev,
[eventName]: value,
}));
setHasChanges(true);
};
// Apply only property/event changes
const handleApplyPropChanges = () => {
if (!selectedComponent) return;
// Combine all changes into a single update object
const allUpdates = {
...pendingProperties,
...pendingEvents,
};
// Apply property and event changes together
if (Object.keys(allUpdates).length > 0) {
onPropertiesChange(selectedComponent.id, allUpdates);
}
// Reset pending changes
setPendingProperties({});
setPendingEvents({});
setHasChanges(false);
};
// Apply only hook changes
const handleApplyHookChanges = () => {
if (!selectedComponent) return;
const hookToggles = Object.entries(pendingHooks).map(
([hookType, enabled]) => ({
componentId: selectedComponent.id,
hookType,
enabled,
})
);
if (hookToggles.length > 1) {
onMultipleHookToggle(hookToggles);
} else if (hookToggles.length === 1) {
const { componentId, hookType, enabled } = hookToggles[0];
onHookToggle(componentId, hookType, enabled);
}
// Reset pending changes
setPendingHooks({});
setHasHookChanges(false);
};
// Reset all pending changes
const handleResetChanges = () => {
setPendingProperties({});
setPendingEvents({});
setPendingHooks({});
setHasChanges(false);
setHasHookChanges(false);
};
const openTailwindModal = (propertyName: string) => {
setCurrentTailwindProperty(propertyName);
setTailwindModalOpen(true);
};
const handleTailwindClassSelect = (className: string) => {
const currentValue =
pendingProperties[currentTailwindProperty] ||
selectedComponent?.props[currentTailwindProperty] ||
"";
const newValue = currentValue ? `${currentValue} ${className}` : className;
handleLocalPropertyChange(currentTailwindProperty, newValue);
// Don't close modal - let user continue selecting
};
const renderPropertyControl = (property: PropertyInfo) => {
// Handle children property specially - get from children, not props
const currentValue =
property.name === "children"
? typeof selectedComponent?.children === "string"
? selectedComponent.children
: selectedComponent?.props[property.name] || property.value
: selectedComponent?.props[property.name] || property.value;
// Use pending value if available, otherwise use current value
const value =
pendingProperties[property.name] !== undefined
? pendingProperties[property.name]
: currentValue;
const isTailwindProperty = ["className", "class", "css"].includes(
property.name
);
const isColorProperty = property.name.toLowerCase().includes("color");
// Don't show children property if component has nested elements
if (
property.name === "children" &&
Array.isArray(selectedComponent?.children)
) {
return null;
}
let arrayError = "";
let arrayInputValue = "";
if (property.type === "array") {
try {
arrayInputValue = JSON.stringify(value, null, 2);
} catch (e) {
arrayInputValue = "";
arrayError = "Array verisi gösterilemiyor.";
}
}
return (
<div key={property.name} className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
{property.name}
{property.description && (
<span className="text-gray-500 text-xs ml-1">
({property.description})
</span>
)}
{pendingProperties[property.name] !== undefined && (
<span className="text-orange-500 text-xs ml-1">(değiştirildi)</span>
)}
</label>
<div className="flex gap-2 flex-col">
{property.type === "boolean" && (
<label className="flex items-center">
<input
type="checkbox"
checked={Boolean(value)}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.checked)
}
className="mr-2"
/>
<span className="text-sm text-gray-600">{property.name}</span>
</label>
)}
{property.type === "select" && property.options && (
<select
value={value || ""}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select {property.name}</option>
{property.options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
)}
{property.type === "function" && (
<>
<input
type="text"
value={value || ""}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`Enter ${property.name}`}
/>
{isTailwindProperty && (
<button
onClick={() => openTailwindModal(property.name)}
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm"
title="Select Tailwind Classes"
>
TW
</button>
)}
{isColorProperty && (
<input
type="color"
value={value || "#000000"}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="w-10 h-10 border border-gray-300 rounded-md"
/>
)}
</>
)}
{property.type === "string" && (
<>
<input
type="text"
value={value || ""}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`Enter ${property.name}`}
/>
{isTailwindProperty && (
<button
onClick={() => openTailwindModal(property.name)}
className="px-3 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm"
title="Select Tailwind Classes"
>
TW
</button>
)}
{isColorProperty && (
<input
type="color"
value={value || "#000000"}
onChange={(e) =>
handleLocalPropertyChange(property.name, e.target.value)
}
className="w-10 h-10 border border-gray-300 rounded-md"
/>
)}
</>
)}
{property.type === "number" && (
<input
type="number"
value={value || 0}
onChange={(e) =>
handleLocalPropertyChange(property.name, Number(e.target.value))
}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
)}
{property.type === "array" && (
<>
<textarea
className="flex-1 px-3 py-2 border border-gray-300 rounded-md font-mono text-xs focus:outline-none focus:ring-2 focus:ring-blue-500"
rows={Math.max(3, arrayInputValue.split('\n').length)}
value={arrayInputValue}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
handleLocalPropertyChange(property.name, parsed);
arrayError = "";
} catch (err) {
arrayError = "Geçersiz JSON formatı";
}
}}
placeholder="[\n { ... }\n]"
spellCheck={false}
/>
{arrayError && (
<span className="text-xs text-red-500">{arrayError}</span>
)}
</>
)}
</div>
</div>
);
};
const renderHookControl = (hook: HookInfo) => {
const currentlyActive = activeHooks.has(hook.type);
const isActive =
pendingHooks[hook.type] !== undefined
? pendingHooks[hook.type]
: currentlyActive;
return (
<div key={hook.name} className="mb-4">
<label className="flex items-center">
<input
type="checkbox"
checked={isActive}
onChange={(e) => handleLocalHookToggle(hook.type, e.target.checked)}
className="mr-2"
/>
<span className="text-sm font-medium text-gray-700">
{hook.name} ({hook.type})
</span>
{pendingHooks[hook.type] !== undefined && (
<span className="text-orange-500 text-xs ml-1">(değiştirildi)</span>
)}
</label>
</div>
);
};
if (!selectedComponent) {
return (
<div className="h-full bg-gray-50 p-4">
<div className="text-center text-gray-500 mt-8">
<div className="text-4xl mb-4">🎯</div>
<h3 className="text-lg font-medium mb-2">No Component Selected</h3>
<p className="text-sm">
Select a component from the editor to edit its properties
</p>
</div>
</div>
);
}
function getDynamicProperties(): PropertyInfo[] {
if (!selectedComponent) return [];
const allDefinitionProps = componentDefinition?.properties || [];
// Sadece tanımdan gelen properties
const defProps = allDefinitionProps.filter(
(p: any) => p.category === "properties"
);
// allDefinitionNames prop isimlerini belirle
const allDefinitionNames = new Set(allDefinitionProps.map((p:any) => p.name));
// Koddan gelen tüm props (id hariç), styling hariç, events hariç
const codeProps = Object.entries(selectedComponent.props || {})
.filter(([name]) => name !== "id" && !allDefinitionNames.has(name))
.map(([name, value]) => {
let type: PropertyInfo["type"] = "string";
if (typeof value === "boolean") type = "boolean";
else if (typeof value === "number") type = "number";
else if (typeof value === "function") type = "function";
else if (Array.isArray(value)) type = "array";
else if (typeof value === "object" && value !== null) type = "object";
return {
name,
type,
value,
category: "properties",
} as PropertyInfo;
});
// Merge - öncelik kodda olanlar
const merged: PropertyInfo[] = [];
const seen = new Set<string>();
for (const prop of codeProps) {
merged.push(prop);
seen.add(prop.name);
}
for (const prop of defProps) {
if (!seen.has(prop.name)) {
merged.push(prop);
}
}
return merged;
}
const properties = getDynamicProperties();
const hooks = componentDefinition?.hooks || [];
const styling =
componentDefinition?.properties?.filter((p: any) => p.category === "styling") ||
[];
const events =
componentDefinition?.properties?.filter((p: any) => p.category === "events") ||
[];
return (
<div className="w-full text-white flex flex-col h-full">
{/* Header */}
<div className="border-b bg-gray-50 flex items-center justify-between">
<div>
{(hasChanges || hasHookChanges) && (
<p className="text-sm text-orange-600 mt-1">
Bekleyen değişiklikler var
</p>
)}
{/* Tabs */}
<div className="flex gap-2 mt-4">
<button
className={`px-3 py-1 rounded-t-md font-medium border-b-2 transition-colors ${
activeTab === "props"
? "border-blue-500 text-blue-700 bg-white"
: "border-transparent text-gray-500 bg-gray-100"
}`}
onClick={() => setActiveTab("props")}
>
Properties
</button>
<button
className={`px-3 py-1 rounded-t-md font-medium border-b-2 transition-colors ${
activeTab === "hooks"
? "border-blue-500 text-blue-700 bg-white"
: "border-transparent text-gray-500 bg-gray-100"
}`}
onClick={() => setActiveTab("hooks")}
>
Hooks
</button>
</div>
</div>
{/* Sil Butonu */}
<Button
variant="solid"
size="sm"
className="mr-2 px-3 py-1 rounded bg-red-500 text-white hover:bg-red-600 transition-colors text-sm"
onClick={() => {
if (selectedComponent) {
if (
window.confirm(
"Seçili komponenti silmek istediğinize emin misiniz?"
)
) {
onDeleteComponent(selectedComponent.id);
}
}
}}
title="Komponenti Sil"
>
Sil
</Button>
</div>
{/* Footer Action Buttons - her iki tabda da sabit */}
<div className="p-4 border-t">
<div className="flex gap-2">
<Button
size="sm"
variant="solid"
onClick={
activeTab === "props"
? handleApplyPropChanges
: handleApplyHookChanges
}
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
className={`flex-1 rounded-md font-medium transition-colors ${
(activeTab === "props" ? hasChanges : hasHookChanges)
? "bg-green-500 text-white hover:bg-green-600"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
>
Uygula
</Button>
<Button
size="sm"
variant="default"
onClick={
activeTab === "props"
? handleResetChanges
: () => {
setPendingHooks({});
setHasHookChanges(false);
}
}
disabled={activeTab === "props" ? !hasChanges : !hasHookChanges}
className={`flex-1 rounded-md font-medium transition-colors ${
(activeTab === "props" ? hasChanges : hasHookChanges)
? "bg-red-500 text-white hover:bg-red-600"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
>
İptal
</Button>
</div>
</div>
{/* Content */}
{activeTab === "props" && (
<div className="flex-1 text-black overflow-y-auto p-4 max-h-[calc(100vh-200px)]">
<h3 className="text-md font-medium text-gray-800 mb-4">Properties</h3>
{/* Properties */}
{properties.length > 0 && (
<div>{properties.map(renderPropertyControl)}</div>
)}
{/* Events */}
{events.length > 0 && (
<div>
<h3 className="text-md font-medium text-gray-800 mb-4 mt-6">
Events
</h3>
{events.map(renderPropertyControl)}
</div>
)}
{/* Styling */}
{styling.length > 0 && (
<div>
<h3 className="text-md font-medium text-gray-800 mb-4 mt-6">
Styling
</h3>
{styling.map(renderPropertyControl)}
</div>
)}
</div>
)}
{activeTab === "hooks" && (
<div className="flex-1 overflow-y-auto p-4">
{/* Sadece useState ve useRef göster */}
{hooks
.filter((h: any) => h.type === "useState" || h.type === "useRef")
.map(renderHookControl)}
</div>
)}
{/* Tailwind Modal */}
<TailwindModal
isOpen={tailwindModalOpen}
onClose={() => setTailwindModalOpen(false)}
onSelectClass={handleTailwindClassSelect}
currentValue={
pendingProperties[currentTailwindProperty] ||
(selectedComponent &&
selectedComponent.props[currentTailwindProperty]) ||
""
}
/>
</div>
);
};
export default PropertyPanel;