Merge remote-tracking branch 'origin/main-dev' into main-demo
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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))}
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
2
app/src/types/simulationDashboard.d.ts
vendored
2
app/src/types/simulationDashboard.d.ts
vendored
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user