Merge remote-tracking branch 'origin/main-dev' into main-demo

This commit is contained in:
2025-12-20 17:41:30 +05:30
10 changed files with 441 additions and 243 deletions

View File

@@ -55,6 +55,7 @@ const DashboardEditor: React.FC = () => {
peekUpdateGraphData, peekUpdateGraphData,
peekUpdateGraphTitle, peekUpdateGraphTitle,
peekUpdateGraphType, peekUpdateGraphType,
peekUpdateTextValue,
peekUpdateDataType, peekUpdateDataType,
peekUpdateCommonValue, peekUpdateCommonValue,
peekUpdateDataValue, peekUpdateDataValue,
@@ -611,6 +612,13 @@ const DashboardEditor: React.FC = () => {
await updateBackend(updatedBlock); await updateBackend(updatedBlock);
} }
}} }}
updateTextValue={async (blockId, elementId, textValue) => {
const updatedBlocks = peekUpdateTextValue(blockId, elementId, textValue);
const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId);
if (updatedBlock) {
await updateBackend(updatedBlock);
}
}}
updateDataType={async (blockId, elementId, dataType) => { updateDataType={async (blockId, elementId, dataType) => {
const updatedBlocks = peekUpdateDataType(blockId, elementId, dataType); const updatedBlocks = peekUpdateDataType(blockId, elementId, dataType);
const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId); const updatedBlock = getBlockFromPeekedBlocks(updatedBlocks, blockId);

View File

@@ -21,10 +21,7 @@ interface BlockEditorProps {
updateBlockStyle: (blockId: string, style: React.CSSProperties) => void; updateBlockStyle: (blockId: string, style: React.CSSProperties) => void;
updateBlockSize: (blockId: string, size: { width: number; height: number }) => void; updateBlockSize: (blockId: string, size: { width: number; height: number }) => void;
updateBlockPosition: (blockId: string, position: { x: number; y: number }) => void; updateBlockPosition: (blockId: string, position: { x: number; y: number }) => void;
updateBlockPositionType: ( updateBlockPositionType: (blockId: string, positionType: "relative" | "absolute" | "fixed") => void;
blockId: string,
positionType: "relative" | "absolute" | "fixed"
) => void;
updateBlockZIndex: (blockId: string, zIndex: number) => void; updateBlockZIndex: (blockId: string, zIndex: number) => void;
handleRemoveBlock: (blockId: string) => void; handleRemoveBlock: (blockId: string) => void;
} }
@@ -35,6 +32,7 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
selectedBlock, selectedBlock,
updateBlockStyle, updateBlockStyle,
updateBlockSize, updateBlockSize,
updateBlockPosition,
updateBlockPositionType, updateBlockPositionType,
updateBlockZIndex, updateBlockZIndex,
handleRemoveBlock, handleRemoveBlock,
@@ -42,56 +40,130 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
const [color, setColor] = useState("#000000"); const [color, setColor] = useState("#000000");
useEffect(() => { useEffect(() => {
setColor( setColor(rgbaToHex(getCurrentBlockStyleValue(currentBlock, "backgroundColor") || "#000000"));
rgbaToHex(
getCurrentBlockStyleValue(currentBlock, "backgroundColor") || "#000000"
)
);
}, [currentBlock]); }, [currentBlock]);
// Use position from VisualizationStore // Use position from VisualizationStore
const { editorPosition, setEditorPosition } = useVisualizationStore(); const { editorPosition, setEditorPosition } = useVisualizationStore();
const panelRef = useRef<HTMLDivElement | null>(null); const panelRef = useRef<HTMLDivElement | null>(null);
const initialPositionRef = useRef<{ x: number; y: number } | null>(null); const initialPositionRef = useRef<{ xPercent: number; yPercent: number } | null>(null);
const draggingRef = useRef(false); const draggingRef = useRef(false);
const startXRef = useRef(0); const startXRef = useRef(0);
const startYRef = useRef(0); const startYRef = useRef(0);
const startLeftRef = useRef(0); const startLeftRef = useRef(0);
const startTopRef = useRef(0); const startTopRef = useRef(0);
const position = editorPosition || { x: 100, y: 80 }; // Get panel dimensions
const setPosition = (newPosition: { x: number; y: number }) => { const getPanelDimensions = () => {
setEditorPosition(newPosition); const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current);
const width = panelEl?.offsetWidth || 300;
const height = panelEl?.offsetHeight || 300;
return { width, height };
}; };
// Convert percentage position to pixels for rendering
const getPixelPosition = (): { x: number; y: number } => {
if (!editorPosition) {
return { x: 100, y: 80 };
}
const { width, height } = getPanelDimensions();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let x = (editorPosition.xPercent / 100) * viewportWidth;
let y = (editorPosition.yPercent / 100) * viewportHeight;
// Ensure panel stays within bounds
const maxX = viewportWidth - width - 8;
const maxY = viewportHeight - height - 8;
x = Math.max(0, Math.min(x, maxX));
y = Math.max(0, Math.min(y, maxY));
return { x, y };
};
const position = getPixelPosition();
// Convert pixel position to percentage and save to store
const setPositionFromPixels = (pixelX: number, pixelY: number) => {
const { width, height } = getPanelDimensions();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Calculate percentage, ensuring panel stays within bounds
const maxXPercent = ((viewportWidth - width) / viewportWidth) * 100;
const maxYPercent = ((viewportHeight - height) / viewportHeight) * 100;
const xPercent = Math.max(0, Math.min((pixelX / viewportWidth) * 100, maxXPercent));
const yPercent = Math.max(0, Math.min((pixelY / viewportHeight) * 100, maxYPercent));
setEditorPosition({ xPercent, yPercent });
};
// Initialize position on mount
// compute exact initial position once we have panel dimensions // compute exact initial position once we have panel dimensions
useEffect(() => { useEffect(() => {
if (!editorPosition) { if (!editorPosition) {
const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); const { width } = getPanelDimensions();
const width = panelEl?.offsetWidth || 300; const defaultX = Math.max(0, window.innerWidth - width - 40);
const nx = Math.max(0, window.innerWidth - width - 40); const defaultY = 80;
const ny = 80;
initialPositionRef.current = { x: nx, y: ny }; setPositionFromPixels(defaultX, defaultY);
setPosition({ x: nx, y: ny }); initialPositionRef.current = {
xPercent: (defaultX / window.innerWidth) * 100,
yPercent: (defaultY / window.innerHeight) * 100,
};
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// Handle window resize - update position to maintain relative placement
useEffect(() => { useEffect(() => {
return () => { const handleResize = () => {
// cleanup in case if (editorPosition) {
draggingRef.current = false; // Force re-render with updated pixel position
// The percentage stays the same, but pixels recalculate
setEditorPosition({ ...editorPosition });
}
}; };
}, []);
const onPointerMove = (ev: PointerEvent) => {
if (!draggingRef.current) return;
const dx = ev.clientX - startXRef.current;
const dy = ev.clientY - startYRef.current;
const { width, height } = getPanelDimensions();
const maxX = window.innerWidth - width - 8;
const maxY = window.innerHeight - height - 8;
let nx = startLeftRef.current + dx;
let ny = startTopRef.current + dy;
if (nx < 0) nx = 0;
if (ny < 0) ny = 0;
if (nx > maxX) nx = maxX;
if (ny > maxY) ny = maxY;
setPositionFromPixels(nx, ny);
};
const onPointerUp = () => {
draggingRef.current = false;
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp);
};
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp);
};
}, [editorPosition, setEditorPosition]);
const startDrag = (ev: React.PointerEvent) => { const startDrag = (ev: React.PointerEvent) => {
if (ev.detail > 1) return; ev.preventDefault();
const panel = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); const panel = panelRef.current || (blockEditorRef && (blockEditorRef as any).current);
if (!panel) return; if (!panel) return;
panel.setPointerCapture?.(ev.pointerId); panel.setPointerCapture?.(ev.pointerId);
draggingRef.current = true; draggingRef.current = true;
startXRef.current = ev.clientX; startXRef.current = ev.clientX;
startYRef.current = ev.clientY; startYRef.current = ev.clientY;
@@ -102,28 +174,25 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
if (!draggingRef.current) return; if (!draggingRef.current) return;
const dx = e.clientX - startXRef.current; const dx = e.clientX - startXRef.current;
const dy = e.clientY - startYRef.current; const dy = e.clientY - startYRef.current;
const { width, height } = getPanelDimensions();
const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current);
const width = panelEl?.offsetWidth || 300;
const height = panelEl?.offsetHeight || 300;
const maxX = window.innerWidth - width - 8; const maxX = window.innerWidth - width - 8;
const maxY = window.innerHeight - height - 8; const maxY = window.innerHeight - height - 8;
let nx = startLeftRef.current + dx; let nx = startLeftRef.current + dx;
let ny = startTopRef.current + dy; let ny = startTopRef.current + dy;
if (nx < 0) nx = 0;
nx = Math.max(0, Math.min(nx, maxX)); if (ny < 0) ny = 0;
ny = Math.max(0, Math.min(ny, maxY)); if (nx > maxX) nx = maxX;
if (ny > maxY) ny = maxY;
setPosition({ x: nx, y: ny }); setPositionFromPixels(nx, ny);
}; };
const onPointerUp = (e: PointerEvent) => { const onPointerUp = (e: PointerEvent) => {
draggingRef.current = false; draggingRef.current = false;
window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp); window.removeEventListener("pointerup", onPointerUp);
panel.releasePointerCapture?.(e.pointerId); try {
(panel as Element).releasePointerCapture?.((e as any).pointerId);
} catch (err) { }
}; };
window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointermove", onPointerMove);
@@ -132,35 +201,26 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
const resetPosition = () => { const resetPosition = () => {
if (initialPositionRef.current) { if (initialPositionRef.current) {
setPosition(initialPositionRef.current); setEditorPosition(initialPositionRef.current);
return; return;
} }
const panelEl = panelRef.current || (blockEditorRef && (blockEditorRef as any).current); const { width } = getPanelDimensions();
const width = panelEl?.offsetWidth || 300; const defaultX = Math.max(0, window.innerWidth - width - 40);
const nx = Math.max(0, window.innerWidth - width - 40); setPositionFromPixels(defaultX, 80);
setPosition({ x: nx, y: 80 });
}; };
return ( return (
<div <div
ref={(el) => { ref={(el) => {
panelRef.current = el; panelRef.current = el;
if ( if (blockEditorRef && typeof blockEditorRef === "object" && "current" in blockEditorRef) {
blockEditorRef &&
typeof blockEditorRef === "object" &&
"current" in blockEditorRef
) {
(blockEditorRef as any).current = el; (blockEditorRef as any).current = el;
} }
}} }}
className="panel block-editor-panel" className="panel block-editor-panel"
style={{ position: "fixed", left: position.x, top: position.y, zIndex: 999 }} style={{ position: "fixed", left: position.x, top: position.y, zIndex: 999 }}
> >
<div <div className={`free-move-button`} onDoubleClick={resetPosition} onPointerDown={startDrag}>
className={`free-move-button`}
onDoubleClick={resetPosition}
onPointerDown={startDrag}
>
<ResizeHeightIcon /> <ResizeHeightIcon />
</div> </div>
@@ -177,48 +237,14 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
</div> </div>
<div className="design-section-wrapper"> <div className="design-section-wrapper">
<div className="design-section">
<div className="section-header">Size</div>
<div className="select-type">
<InputWithDropDown
label="Width"
value={String(currentBlock.size?.width || 400)} // Ensure the value is a string
placeholder={"Width"}
onChange={(newValue) => {
updateBlockSize(selectedBlock, {
...currentBlock.size!,
width: Number(newValue), // Make sure to convert the string back to a number here
});
}}
/>
<InputWithDropDown
label="Height"
value={String(currentBlock.size?.height || 300)}
placeholder={"Width"}
onChange={(newValue) => {
updateBlockSize(selectedBlock, {
...currentBlock.size!,
height: Number(newValue),
});
}}
/>
</div>
</div>
<div className="design-section"> <div className="design-section">
<div className="section-header">Position</div> <div className="section-header">Position</div>
<div className="select-type"> <div className="select-type">
{["relative", "absolute"].map((position) => ( {["relative", "absolute"].map((position) => (
<div <div
key={position} key={position}
className={`type ${currentBlock.positionType === position ? "active" : "" className={`type ${currentBlock.positionType === position ? "active" : ""}`}
}`} onClick={() => updateBlockPositionType(selectedBlock, position as "relative" | "absolute" | "fixed")}
onClick={() =>
updateBlockPositionType(
selectedBlock,
position as "relative" | "absolute" | "fixed"
)
}
> >
{position.charAt(0).toUpperCase() + position.slice(1)} {position.charAt(0).toUpperCase() + position.slice(1)}
</div> </div>
@@ -227,48 +253,16 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
<div className="position-canvas"> <div className="position-canvas">
<div className="canvas"> <div className="canvas">
<div className="value padding-top"> <div className="value padding-top">
<RenameInput <RenameInput value={getCurrentBlockStyleValue(currentBlock, "padding") ? String(getCurrentBlockStyleValue(currentBlock, "padding")) : "120"} />
value={
getCurrentBlockStyleValue(currentBlock, "padding")
? String(
getCurrentBlockStyleValue(currentBlock, "padding")
)
: "120"
}
/>
</div> </div>
<div className="value padding-right"> <div className="value padding-right">
<RenameInput <RenameInput value={getCurrentBlockStyleValue(currentBlock, "padding") ? String(getCurrentBlockStyleValue(currentBlock, "padding")) : "120"} />
value={
getCurrentBlockStyleValue(currentBlock, "padding")
? String(
getCurrentBlockStyleValue(currentBlock, "padding")
)
: "120"
}
/>
</div> </div>
<div className="value padding-bottom"> <div className="value padding-bottom">
<RenameInput <RenameInput value={getCurrentBlockStyleValue(currentBlock, "padding") ? String(getCurrentBlockStyleValue(currentBlock, "padding")) : "120"} />
value={
getCurrentBlockStyleValue(currentBlock, "padding")
? String(
getCurrentBlockStyleValue(currentBlock, "padding")
)
: "120"
}
/>
</div> </div>
<div className="value padding-left"> <div className="value padding-left">
<RenameInput <RenameInput value={getCurrentBlockStyleValue(currentBlock, "padding") ? String(getCurrentBlockStyleValue(currentBlock, "padding")) : "120"} />
value={
getCurrentBlockStyleValue(currentBlock, "padding")
? String(
getCurrentBlockStyleValue(currentBlock, "padding")
)
: "120"
}
/>
</div> </div>
</div> </div>
</div> </div>
@@ -279,9 +273,7 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
label="Layer" label="Layer"
value={String(currentBlock.zIndex ?? 1)} value={String(currentBlock.zIndex ?? 1)}
placeholder={"Layer"} placeholder={"Layer"}
onChange={(newValue) => onChange={(newValue) => updateBlockZIndex(selectedBlock, Number(newValue))}
updateBlockZIndex(selectedBlock, Number(newValue))
}
/> />
<button <button
className="increase-z" className="increase-z"
@@ -302,7 +294,7 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
<button <button
className="reset" className="reset"
onClick={() => { onClick={() => {
updateBlockZIndex(selectedBlock, Number(1)) updateBlockZIndex(selectedBlock, Number(1));
}} }}
> >
<ResetIcon /> <ResetIcon />
@@ -311,16 +303,63 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
</div> </div>
</div> </div>
<div className="design-section"> <div className="design-section appearance">
<div className="section-header">Apperance</div> <div className="section-header">Appearance</div>
<div className="design-datas-wrapper">
{currentBlock.positionType === "absolute" && (
<>
<InputWithDropDown
label="X-Position"
value={String(currentBlock.position?.x ?? 0)}
placeholder={"X"}
onChange={(newValue: string) => {
updateBlockPosition(selectedBlock, {
...(currentBlock.position || { x: 0, y: 0 }),
x: Number(newValue),
});
}}
/>
<InputWithDropDown
label="Y-Position"
value={String(currentBlock.position?.y ?? 0)}
placeholder={"Y"}
onChange={(newValue: string) => {
updateBlockPosition(selectedBlock, {
...(currentBlock.position || { x: 0, y: 0 }),
y: Number(newValue),
});
}}
/>
</>
)}
<InputWithDropDown
label="Width"
value={String(currentBlock.size?.width || 400)}
placeholder={"Width"}
onChange={(newValue) => {
updateBlockSize(selectedBlock, {
...currentBlock.size!,
width: Number(newValue),
});
}}
/>
<InputWithDropDown
label="Height"
value={String(currentBlock.size?.height || 300)}
placeholder={"Height"}
onChange={(newValue) => {
updateBlockSize(selectedBlock, {
...currentBlock.size!,
height: Number(newValue),
});
}}
/>
</div>
<InputRange <InputRange
label={"Border Radius"} label={"Border Radius"}
min={0} min={0}
max={100} max={100}
value={( value={parseInt(getCurrentBlockStyleValue(currentBlock, "borderRadius")) || 8}
parseInt(getCurrentBlockStyleValue(currentBlock, "borderRadius")) ||
8
)}
onChange={(newValue) => { onChange={(newValue) => {
updateBlockStyle(selectedBlock, { updateBlockStyle(selectedBlock, {
borderRadius: Number(newValue), borderRadius: Number(newValue),
@@ -338,43 +377,21 @@ const BlockEditor: React.FC<BlockEditorProps> = ({
onChange={(value) => { onChange={(value) => {
setColor(value); setColor(value);
if (/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(value)) { if (/^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(value)) {
handleBackgroundColorChange( handleBackgroundColorChange(currentBlock, selectedBlock, updateBlockStyle, value);
currentBlock,
selectedBlock,
updateBlockStyle,
value
);
} }
}} }}
onUpdate={(value) => { onUpdate={(value) => {
handleBackgroundColorChange( handleBackgroundColorChange(currentBlock, selectedBlock, updateBlockStyle, value);
currentBlock,
selectedBlock,
updateBlockStyle,
value
);
}} }}
/> />
<InputRange <InputRange
label={"Opacity"} label={"Opacity"}
min={0} min={0}
max={1} max={1}
step={0.1} step={0.1}
value={Math.round( value={Math.round(getAlphaFromRgba(getCurrentBlockStyleValue(currentBlock, "backgroundColor")))}
getAlphaFromRgba( onChange={(value: number) => handleBackgroundAlphaChange(currentBlock, selectedBlock, updateBlockStyle, Number(value))}
getCurrentBlockStyleValue(currentBlock, "backgroundColor")
)
)}
onChange={(value: number) =>
handleBackgroundAlphaChange(
currentBlock,
selectedBlock,
updateBlockStyle,
Number(value)
)
}
/> />
<InputRange <InputRange
label={"Blur"} label={"Blur"}

View File

@@ -25,6 +25,7 @@ interface ElementDesignProps {
updateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => void; updateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => void;
updateGraphTitle: (blockId: string, elementId: string, title: string) => void; updateGraphTitle: (blockId: string, elementId: string, title: string) => void;
updateGraphType: (blockId: string, elementId: string, type: GraphTypes) => void; updateGraphType: (blockId: string, elementId: string, type: GraphTypes) => void;
updateTextValue: (blockId: string, elementId: string, textValue: string) => void;
updateDataType: (blockId: string, elementId: string, dataType: "single-machine" | "multiple-machine") => void; updateDataType: (blockId: string, elementId: string, dataType: "single-machine" | "multiple-machine") => void;
updateCommonValue: (blockId: string, elementId: string, commonValue: string) => void; updateCommonValue: (blockId: string, elementId: string, commonValue: string) => void;
updateDataValue: (blockId: string, elementId: string, dataValue: string | string[]) => void; updateDataValue: (blockId: string, elementId: string, dataValue: string | string[]) => void;
@@ -45,6 +46,7 @@ const ElementDesign: React.FC<ElementDesignProps> = ({
updateElementZIndex, updateElementZIndex,
updateGraphTitle, updateGraphTitle,
updateGraphType, updateGraphType,
updateTextValue,
}) => { }) => {
const [color, setColor] = useState("#000000"); const [color, setColor] = useState("#000000");
@@ -52,19 +54,22 @@ const ElementDesign: React.FC<ElementDesignProps> = ({
setColor(rgbaToHex(getCurrentElementStyleValue(currentElement, "backgroundColor") || "#000000")); setColor(rgbaToHex(getCurrentElementStyleValue(currentElement, "backgroundColor") || "#000000"));
}, [currentElement]); }, [currentElement]);
const graphOptions = [
{ id: "line", label: "Line Chart" },
{ id: "bar", label: "Bar Chart" },
{ id: "pie", label: "Pie Chart" },
{ id: "area", label: "Area Chart" },
{ id: "radar", label: "Radar Chart" },
];
return ( return (
<div style={{ display: "flex", flexDirection: "column", gap: 6 }} ref={elementEditorRef}> <div style={{ display: "flex", flexDirection: "column", gap: 6 }} ref={elementEditorRef}>
{element?.type === "graph" && ( {element?.type === "graph" && (
<div className="design-section"> <div className="design-section">
<DataSourceSelector <DataSourceSelector
label={"Chart Type"} label={"Chart Type"}
options={[ options={graphOptions}
{ id: "line", label: "Line Chart" }, selected={graphOptions.find((option) => option.id === currentElement.graphType)?.label}
{ id: "bar", label: "Bar Chart" },
{ id: "pie", label: "Pie Chart" },
{ id: "area", label: "Area Chart" },
{ id: "radar", label: "Radar Chart" },
]}
onSelect={(newValue) => { onSelect={(newValue) => {
updateGraphType(selectedBlock, selectedElement, newValue.id as GraphTypes); updateGraphType(selectedBlock, selectedElement, newValue.id as GraphTypes);
}} }}
@@ -73,6 +78,19 @@ const ElementDesign: React.FC<ElementDesignProps> = ({
</div> </div>
)} )}
{element?.type === "text" && (
<div className="design-section">
<InputWithDropDown
label="Text Value"
value={currentElement.textValue || ""}
placeholder={"Enter text..."}
onChange={(newValue: string) => {
updateTextValue(selectedBlock, selectedElement, newValue);
}}
/>
</div>
)}
<div className="design-section"> <div className="design-section">
<div className="section-header">Position</div> <div className="section-header">Position</div>
<div className="select-type"> <div className="select-type">
@@ -218,28 +236,32 @@ const ElementDesign: React.FC<ElementDesignProps> = ({
<div className="design-section appearance"> <div className="design-section appearance">
<div className="section-header">Appearance</div> <div className="section-header">Appearance</div>
<div className="design-datas-wrapper"> <div className="design-datas-wrapper">
<InputWithDropDown {currentElement.positionType === "absolute" && (
label="X-Position" <>
value={String(currentElement.position?.x ?? 0)} <InputWithDropDown
placeholder={"X"} label="X-Position"
onChange={(newValue: string) => { value={String(currentElement.position?.x ?? 0)}
updateElementPosition(selectedBlock, selectedElement, { placeholder={"X"}
...(currentElement.position || { x: 0, y: 0 }), onChange={(newValue: string) => {
x: Number(newValue), updateElementPosition(selectedBlock, selectedElement, {
}); ...(currentElement.position || { x: 0, y: 0 }),
}} x: Number(newValue),
/> });
<InputWithDropDown }}
label="Y-Position" />
value={String(currentElement.position?.y ?? 0)} <InputWithDropDown
placeholder={"Y"} label="Y-Position"
onChange={(newValue: string) => { value={String(currentElement.position?.y ?? 0)}
updateElementPosition(selectedBlock, selectedElement, { placeholder={"Y"}
...(currentElement.position || { x: 0, y: 0 }), onChange={(newValue: string) => {
y: Number(newValue), updateElementPosition(selectedBlock, selectedElement, {
}); ...(currentElement.position || { x: 0, y: 0 }),
}} y: Number(newValue),
/> });
}}
/>
</>
)}
<InputWithDropDown <InputWithDropDown
label="Width" label="Width"
value={String(currentElement.size?.width ?? (currentElement.type === "graph" ? 400 : 200))} value={String(currentElement.size?.width ?? (currentElement.type === "graph" ? 400 : 200))}

View File

@@ -27,7 +27,6 @@ const ElementDropdown: React.FC<ElementDropdownProps> = ({ showElementDropdown,
key={elementType.label} key={elementType.label}
onClick={() => { onClick={() => {
handleAddElement(showElementDropdown, elementType.type, elementType.graphType); handleAddElement(showElementDropdown, elementType.type, elementType.graphType);
console.log('showElementDropdown: ', showElementDropdown);
}} }}
className="dropdown-button" className="dropdown-button"
> >

View File

@@ -25,6 +25,7 @@ interface ElementEditorProps {
updateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => void; updateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => void;
updateGraphTitle: (blockId: string, elementId: string, title: string) => void; updateGraphTitle: (blockId: string, elementId: string, title: string) => void;
updateGraphType: (blockId: string, elementId: string, type: GraphTypes) => void; updateGraphType: (blockId: string, elementId: string, type: GraphTypes) => void;
updateTextValue: (blockId: string, elementId: string, textValue: string) => void;
updateDataType: (blockId: string, elementId: string, dataType: "single-machine" | "multiple-machine") => void; updateDataType: (blockId: string, elementId: string, dataType: "single-machine" | "multiple-machine") => void;
updateCommonValue: (blockId: string, elementId: string, commonValue: string) => void; updateCommonValue: (blockId: string, elementId: string, commonValue: string) => void;
updateDataValue: (blockId: string, elementId: string, dataValue: string | string[]) => void; updateDataValue: (blockId: string, elementId: string, dataValue: string | string[]) => void;
@@ -46,6 +47,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
updateGraphData, updateGraphData,
updateGraphTitle, updateGraphTitle,
updateGraphType, updateGraphType,
updateTextValue,
updateDataType, updateDataType,
updateCommonValue, updateCommonValue,
updateDataValue, updateDataValue,
@@ -63,26 +65,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
// Use shared position from VisualizationStore // Use shared position from VisualizationStore
const { editorPosition, setEditorPosition } = useVisualizationStore(); const { editorPosition, setEditorPosition } = useVisualizationStore();
const panelRef = useRef<HTMLDivElement | null>(null); const panelRef = useRef<HTMLDivElement | null>(null);
const initialPositionRef = useRef<{ x: number; y: number } | null>(null); const initialPositionRef = useRef<{ xPercent: number; yPercent: number } | null>(null);
// Compute position from store or initialize
const position = editorPosition || { x: 100, y: 80 };
const setPosition = (newPosition: { x: number; y: number }) => {
setEditorPosition(newPosition);
};
// On mount, compute exact initial position based on panel width and store it
useEffect(() => {
if (!editorPosition) {
const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current);
const width = panelEl?.offsetWidth || 300;
const nx = Math.max(0, window.innerWidth - width - 40);
const ny = 80;
initialPositionRef.current = { x: nx, y: ny };
setPosition({ x: nx, y: ny });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const draggingRef = useRef(false); const draggingRef = useRef(false);
const startXRef = useRef(0); const startXRef = useRef(0);
@@ -90,14 +73,86 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
const startLeftRef = useRef(0); const startLeftRef = useRef(0);
const startTopRef = useRef(0); const startTopRef = useRef(0);
// Get panel dimensions
const getPanelDimensions = () => {
const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current);
const width = panelEl?.offsetWidth || 300;
const height = panelEl?.offsetHeight || 300;
return { width, height };
};
// Convert percentage position to pixels for rendering
const getPixelPosition = (): { x: number; y: number } => {
if (!editorPosition) {
return { x: 100, y: 80 };
}
const { width, height } = getPanelDimensions();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let x = (editorPosition.xPercent / 100) * viewportWidth;
let y = (editorPosition.yPercent / 100) * viewportHeight;
// Ensure panel stays within bounds
const maxX = viewportWidth - width - 8;
const maxY = viewportHeight - height - 8;
x = Math.max(0, Math.min(x, maxX));
y = Math.max(0, Math.min(y, maxY));
return { x, y };
};
const position = getPixelPosition();
// Convert pixel position to percentage and save to store
const setPositionFromPixels = (pixelX: number, pixelY: number) => {
const { width, height } = getPanelDimensions();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Calculate percentage, ensuring panel stays within bounds
const maxXPercent = ((viewportWidth - width) / viewportWidth) * 100;
const maxYPercent = ((viewportHeight - height) / viewportHeight) * 100;
const xPercent = Math.max(0, Math.min((pixelX / viewportWidth) * 100, maxXPercent));
const yPercent = Math.max(0, Math.min((pixelY / viewportHeight) * 100, maxYPercent));
setEditorPosition({ xPercent, yPercent });
};
// On mount, compute exact initial position based on panel width and store it
useEffect(() => { useEffect(() => {
if (!editorPosition) {
const { width } = getPanelDimensions();
const defaultX = Math.max(0, window.innerWidth - width - 40);
const defaultY = 80;
setPositionFromPixels(defaultX, defaultY);
initialPositionRef.current = {
xPercent: (defaultX / window.innerWidth) * 100,
yPercent: (defaultY / window.innerHeight) * 100,
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Handle window resize - update position to maintain relative placement
useEffect(() => {
const handleResize = () => {
if (editorPosition) {
// Force re-render with updated pixel position
// The percentage stays the same, but pixels recalculate
setEditorPosition({ ...editorPosition });
}
};
const onPointerMove = (ev: PointerEvent) => { const onPointerMove = (ev: PointerEvent) => {
if (!draggingRef.current) return; if (!draggingRef.current) return;
const dx = ev.clientX - startXRef.current; const dx = ev.clientX - startXRef.current;
const dy = ev.clientY - startYRef.current; const dy = ev.clientY - startYRef.current;
const panel = panelRef.current; const { width, height } = getPanelDimensions();
const width = panel?.offsetWidth || 300;
const height = panel?.offsetHeight || 300;
const maxX = window.innerWidth - width - 8; const maxX = window.innerWidth - width - 8;
const maxY = window.innerHeight - height - 8; const maxY = window.innerHeight - height - 8;
let nx = startLeftRef.current + dx; let nx = startLeftRef.current + dx;
@@ -106,7 +161,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
if (ny < 0) ny = 0; if (ny < 0) ny = 0;
if (nx > maxX) nx = maxX; if (nx > maxX) nx = maxX;
if (ny > maxY) ny = maxY; if (ny > maxY) ny = maxY;
setPosition({ x: nx, y: ny }); setPositionFromPixels(nx, ny);
}; };
const onPointerUp = () => { const onPointerUp = () => {
@@ -115,11 +170,13 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
window.removeEventListener("pointerup", onPointerUp); window.removeEventListener("pointerup", onPointerUp);
}; };
window.addEventListener("resize", handleResize);
return () => { return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("pointermove", onPointerMove); window.removeEventListener("pointermove", onPointerMove);
window.removeEventListener("pointerup", onPointerUp); window.removeEventListener("pointerup", onPointerUp);
}; };
}, []); }, [editorPosition, setEditorPosition]);
const startDrag = (ev: React.PointerEvent) => { const startDrag = (ev: React.PointerEvent) => {
ev.preventDefault(); ev.preventDefault();
@@ -136,9 +193,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
if (!draggingRef.current) return; if (!draggingRef.current) return;
const dx = e.clientX - startXRef.current; const dx = e.clientX - startXRef.current;
const dy = e.clientY - startYRef.current; const dy = e.clientY - startYRef.current;
const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current); const { width, height } = getPanelDimensions();
const width = panelEl?.offsetWidth || 300;
const height = panelEl?.offsetHeight || 300;
const maxX = window.innerWidth - width - 8; const maxX = window.innerWidth - width - 8;
const maxY = window.innerHeight - height - 8; const maxY = window.innerHeight - height - 8;
let nx = startLeftRef.current + dx; let nx = startLeftRef.current + dx;
@@ -147,7 +202,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
if (ny < 0) ny = 0; if (ny < 0) ny = 0;
if (nx > maxX) nx = maxX; if (nx > maxX) nx = maxX;
if (ny > maxY) ny = maxY; if (ny > maxY) ny = maxY;
setPosition({ x: nx, y: ny }); setPositionFromPixels(nx, ny);
}; };
const onPointerUp = (e: PointerEvent) => { const onPointerUp = (e: PointerEvent) => {
@@ -156,7 +211,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
window.removeEventListener("pointerup", onPointerUp); window.removeEventListener("pointerup", onPointerUp);
try { try {
(panel as Element).releasePointerCapture?.((e as any).pointerId); (panel as Element).releasePointerCapture?.((e as any).pointerId);
} catch (err) { } } catch (err) {}
}; };
window.addEventListener("pointermove", onPointerMove); window.addEventListener("pointermove", onPointerMove);
@@ -165,13 +220,12 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
const resetPosition = () => { const resetPosition = () => {
if (initialPositionRef.current) { if (initialPositionRef.current) {
setPosition(initialPositionRef.current); setEditorPosition(initialPositionRef.current);
return; return;
} }
const panelEl = panelRef.current || (elementEditorRef && (elementEditorRef as any).current); const { width } = getPanelDimensions();
const width = panelEl?.offsetWidth || 300; const defaultX = Math.max(0, window.innerWidth - width - 40);
const nx = Math.max(0, window.innerWidth - width - 40); setPositionFromPixels(defaultX, 80);
setPosition({ x: nx, y: 80 });
}; };
const getAssetDropdownItems = useCallback(() => { const getAssetDropdownItems = useCallback(() => {
@@ -510,6 +564,7 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
updateGraphData={updateGraphData} updateGraphData={updateGraphData}
updateGraphTitle={updateGraphTitle} updateGraphTitle={updateGraphTitle}
updateGraphType={updateGraphType} updateGraphType={updateGraphType}
updateTextValue={updateTextValue}
updateDataType={updateDataType} updateDataType={updateDataType}
updateCommonValue={updateCommonValue} updateCommonValue={updateCommonValue}
updateDataValue={updateDataValue} updateDataValue={updateDataValue}
@@ -549,12 +604,12 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
value={ value={
element.dataBinding?.dataSource element.dataBinding?.dataSource
? { ? {
id: element.dataBinding.dataSource as string, id: element.dataBinding.dataSource as string,
label: label:
getEventByModelUuid(selectedProduct.productUuid, element.dataBinding.dataSource as string)?.modelName ?? getEventByModelUuid(selectedProduct.productUuid, element.dataBinding.dataSource as string)?.modelName ??
(element.dataBinding.dataSource === "global" ? "Global" : ""), (element.dataBinding.dataSource === "global" ? "Global" : ""),
icon: <DeviceIcon />, icon: <DeviceIcon />,
} }
: null : null
} }
onChange={(value) => { onChange={(value) => {
@@ -573,13 +628,13 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
value={ value={
element.dataBinding?.dataValue element.dataBinding?.dataValue
? { ? {
id: element.dataBinding.dataValue as string, id: element.dataBinding.dataValue as string,
label: label:
getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined) getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined)
.flatMap((section) => section.items) .flatMap((section) => section.items)
.find((item) => item.id === element.dataBinding?.dataValue)?.label ?? "", .find((item) => item.id === element.dataBinding?.dataValue)?.label ?? "",
icon: <ParametersIcon />, icon: <ParametersIcon />,
} }
: null : null
} }
onChange={(value) => { onChange={(value) => {

View File

@@ -1,6 +1,11 @@
import type { UIElement } from "../../../../types/exportedTypes"; import type { UIElement } from "../../../../types/exportedTypes";
export const resolveElementValue = (element: UIElement) => { export const resolveElementValue = (element: UIElement) => {
// For text elements, return the textValue
if (element.type === "text") {
return { value: element.textValue || "Text" };
}
if (!element.dataBinding) { if (!element.dataBinding) {
return { value: "No data" }; return { value: "No data" };
} }

View File

@@ -48,6 +48,7 @@ interface SimulationDashboardStore {
updateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => void; updateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => void;
updateGraphTitle: (blockId: string, elementId: string, title: string) => void; updateGraphTitle: (blockId: string, elementId: string, title: string) => void;
updateGraphType: (blockId: string, elementId: string, graphType: GraphTypes) => void; updateGraphType: (blockId: string, elementId: string, graphType: GraphTypes) => void;
updateTextValue: (blockId: string, elementId: string, textValue: string) => void;
// Element swapping // Element swapping
swapElements: (blockId: string, elementId1: string, elementId2: string) => void; swapElements: (blockId: string, elementId1: string, elementId2: string) => void;
@@ -73,6 +74,7 @@ interface SimulationDashboardStore {
peekUpdateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => Block[]; peekUpdateGraphData: (blockId: string, elementId: string, newData: GraphDataPoint[]) => Block[];
peekUpdateGraphTitle: (blockId: string, elementId: string, title: string) => Block[]; peekUpdateGraphTitle: (blockId: string, elementId: string, title: string) => Block[];
peekUpdateGraphType: (blockId: string, elementId: string, graphType: GraphTypes) => Block[]; peekUpdateGraphType: (blockId: string, elementId: string, graphType: GraphTypes) => Block[];
peekUpdateTextValue: (blockId: string, elementId: string, textValue: string) => Block[];
peekUpdateDataType: (blockId: string, elementId: string, dataType: "single-machine" | "multiple-machine") => Block[]; peekUpdateDataType: (blockId: string, elementId: string, dataType: "single-machine" | "multiple-machine") => Block[];
peekUpdateCommonValue: (blockId: string, elementId: string, commonValue: string) => Block[]; peekUpdateCommonValue: (blockId: string, elementId: string, commonValue: string) => Block[];
peekUpdateDataValue: (blockId: string, elementId: string, dataValue: string | string[]) => Block[]; peekUpdateDataValue: (blockId: string, elementId: string, dataValue: string | string[]) => Block[];
@@ -321,6 +323,7 @@ export const createSimulationDashboardStore = () => {
newElement = { newElement = {
...commonProps, ...commonProps,
type: "text", type: "text",
textValue: "Text",
style: { style: {
color: "#ffffff", color: "#ffffff",
fontSize: 14, fontSize: 14,
@@ -487,6 +490,18 @@ export const createSimulationDashboardStore = () => {
}); });
}, },
updateTextValue: (blockId, elementId, textValue) => {
set((state) => {
const block = state.blocks.find((b) => b.blockUuid === blockId);
if (block) {
const element = block.elements.find((el) => el.elementUuid === elementId);
if (element && element.type === "text") {
element.textValue = textValue;
}
}
});
},
// Element swapping // Element swapping
swapElements: (blockId, elementId1, elementId2) => { swapElements: (blockId, elementId1, elementId2) => {
set((state) => { set((state) => {
@@ -685,6 +700,7 @@ export const createSimulationDashboardStore = () => {
newElement = { newElement = {
...commonProps, ...commonProps,
type: "text", type: "text",
textValue: "Text",
style: { style: {
color: "#ffffff", color: "#ffffff",
fontSize: 14, fontSize: 14,
@@ -846,6 +862,18 @@ export const createSimulationDashboardStore = () => {
return blocks; return blocks;
}, },
peekUpdateTextValue: (blockId, elementId, textValue) => {
const blocks = cloneBlocks(get().blocks);
const block = blocks.find((b) => b.blockUuid === blockId);
if (block) {
const element = block.elements.find((el) => el.elementUuid === elementId);
if (element && element.type === "text") {
element.textValue = textValue;
}
}
return blocks;
},
peekUpdateDataType: (blockId: string, elementId: string, dataType: DataType) => { peekUpdateDataType: (blockId: string, elementId: string, dataType: DataType) => {
const blocks = cloneBlocks(get().blocks); const blocks = cloneBlocks(get().blocks);

View File

@@ -1,14 +1,19 @@
import { create } from "zustand"; import { create } from "zustand";
import { immer } from "zustand/middleware/immer"; import { immer } from "zustand/middleware/immer";
/**
* Panel position stored as percentages (0-100) of viewport dimensions
* This ensures panels stay within bounds when screen size changes
*/
interface PanelPosition { interface PanelPosition {
x: number; xPercent: number; // 0-100, percentage from left edge
y: number; yPercent: number; // 0-100, percentage from top edge
} }
interface VisualizationState { interface VisualizationState {
editorPosition: PanelPosition | null; editorPosition: PanelPosition | null;
setEditorPosition: (position: PanelPosition) => void; setEditorPosition: (position: PanelPosition) => void;
resetEditorPosition: () => void;
} }
export const useVisualizationStore = create<VisualizationState>()( export const useVisualizationStore = create<VisualizationState>()(
@@ -18,5 +23,63 @@ export const useVisualizationStore = create<VisualizationState>()(
set((state) => { set((state) => {
state.editorPosition = position; state.editorPosition = position;
}), }),
resetEditorPosition: () =>
set((state) => {
state.editorPosition = null;
}),
})) }))
); );
/**
* Utility functions to convert between pixels and percentages
*/
export const positionUtils = {
/**
* Convert pixel position to percentage position
*/
pixelsToPercent: (pixelX: number, pixelY: number, panelWidth: number, panelHeight: number): PanelPosition => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Calculate percentage, ensuring panel stays within bounds
const maxXPercent = ((viewportWidth - panelWidth) / viewportWidth) * 100;
const maxYPercent = ((viewportHeight - panelHeight) / viewportHeight) * 100;
const xPercent = Math.max(0, Math.min((pixelX / viewportWidth) * 100, maxXPercent));
const yPercent = Math.max(0, Math.min((pixelY / viewportHeight) * 100, maxYPercent));
return { xPercent, yPercent };
},
/**
* Convert percentage position to pixel position
*/
percentToPixels: (position: PanelPosition, panelWidth: number, panelHeight: number): { x: number; y: number } => {
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Calculate pixel position
let x = (position.xPercent / 100) * viewportWidth;
let y = (position.yPercent / 100) * viewportHeight;
// Ensure panel stays within bounds
const maxX = viewportWidth - panelWidth - 8;
const maxY = viewportHeight - panelHeight - 8;
x = Math.max(0, Math.min(x, maxX));
y = Math.max(0, Math.min(y, maxY));
return { x, y };
},
/**
* Get default position (top-right corner with some margin)
*/
getDefaultPosition: (panelWidth: number): PanelPosition => {
const viewportWidth = window.innerWidth;
const defaultX = Math.max(0, viewportWidth - panelWidth - 40);
const defaultY = 80;
return positionUtils.pixelsToPercent(defaultX, defaultY, panelWidth, 300);
},
};

View File

@@ -20,6 +20,7 @@ export type UIElement = {
type: UIType; type: UIType;
graphType?: GraphTypes; graphType?: GraphTypes;
graphTitle?: string; graphTitle?: string;
textValue?: string;
style: ExtendedCSSProperties; style: ExtendedCSSProperties;
dataBinding?: ElementDataBinding; dataBinding?: ElementDataBinding;
position?: Position; position?: Position;

View File

@@ -11,7 +11,7 @@ type ElementDataBinding = {
dataSource?: string | string[]; dataSource?: string | string[];
dataValue?: string | string[]; dataValue?: string | string[];
commonValue?: string; commonValue?: string;
dataType?: "single-machine" | "multiple-machine"; dataType?: DataType;
} }
type Position = { type Position = {