feat: Add ElementEditor component for managing UI element properties and editor panel positioning.

This commit is contained in:
2025-12-22 10:18:28 +05:30
parent 64fc16500d
commit dd5e8b8c42
3 changed files with 283 additions and 212 deletions

View File

@@ -0,0 +1,256 @@
import { AddIcon, DeviceIcon, ParametersIcon } from "../../../icons/ExportCommonIcons";
import DataDetailedDropdown from "../../../ui/inputs/DataDetailedDropdown";
import DataSourceSelector from "../../../ui/inputs/DataSourceSelector";
import InputWithDropDown from "../../../ui/inputs/InputWithDropDown";
import { UIElement } from "../../../../types/exportedTypes";
interface ElementDataProps {
element: UIElement | null;
selectedBlock: string;
selectedElement: string;
updateElementData: (blockId: string, elementId: string, updates: Partial<ElementDataBinding>) => void;
selectedProduct: any;
getEventByModelUuid: (productUuid: string, modelUuid: string) => any;
getAssetDropdownItems: () => Array<{ id: string; label: string; icon: JSX.Element }>;
getLableValueDropdownItems: (assetId: string | undefined) => Array<{ title: string; items: Array<{ id: string; label: string; icon: JSX.Element }> }>;
singleSourceFields: Array<{ id: string; label: string; showEyeDropper: boolean; options: Array<{ id: string; label: string }> }>;
singleValueFields: Array<{ id: string; label: string; showEyeDropper: boolean; options: Array<{ id: string; label: string }> }>;
multipleSourceFields: Array<{ id: string; label: string; showEyeDropper: boolean; options: Array<{ id: string; label: string }> }>;
multipleValueFields: Array<{ id: string; label: string; showEyeDropper: boolean; options: Array<{ id: string; label: string }> }>;
selectDataMapping: "singleMachine" | "multipleMachine";
handleDataTypeSwitch: (newDataType: "singleMachine" | "multipleMachine") => void;
updateDataSource: (blockId: string, elementId: string, dataSource: string | string[]) => void;
updateDataValue: (blockId: string, elementId: string, dataValue: string | string[]) => void;
updateCommonValue: (blockId: string, elementId: string, commonValue: string) => void;
addField?: () => void;
}
const ElementData: React.FC<ElementDataProps> = ({
element,
selectedBlock,
selectedElement,
updateElementData,
selectedProduct,
getEventByModelUuid,
getAssetDropdownItems,
getLableValueDropdownItems,
singleSourceFields = [],
singleValueFields = [],
multipleSourceFields = [],
multipleValueFields = [],
selectDataMapping = "singleMachine",
handleDataTypeSwitch,
updateDataSource,
updateDataValue,
updateCommonValue,
addField,
}) => {
return (
<div className="data-details">
{element?.type === "label-value" && (
<>
<div className="design-section">
<div className="sub-header">Data Handling</div>
<InputWithDropDown
label="Label"
type="string"
value={element.dataBinding?.label || ""}
placeholder={"Label 1"}
onChange={(value) => {
updateElementData(selectedBlock, selectedElement, { label: value });
}}
/>
<div className="data">
<DataDetailedDropdown
title="Data Source"
placeholder="Select assets"
sections={[
{
title: "Global",
items: [{ id: "global", label: "Global", icon: <DeviceIcon /> }],
},
{
title: "Assets",
items: getAssetDropdownItems(),
},
]}
value={
element.dataBinding?.dataSource
? {
id: element.dataBinding.dataSource as string,
label:
getEventByModelUuid(selectedProduct.productUuid, element.dataBinding.dataSource as string)?.modelName ??
(element.dataBinding.dataSource === "global" ? "Global" : ""),
icon: <DeviceIcon />,
}
: null
}
onChange={(value) => {
updateElementData(selectedBlock, selectedElement, { dataSource: value.id });
}}
dropDownHeader={"RT-Data"}
eyedroper={true}
/>
</div>
<div className="data">
<DataDetailedDropdown
title="Data Value"
placeholder="Select Value"
sections={getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined)}
value={
element.dataBinding?.dataValue
? {
id: element.dataBinding.dataValue as string,
label:
getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined)
.flatMap((section) => section.items)
.find((item) => item.id === element.dataBinding?.dataValue)?.label ?? "",
icon: <ParametersIcon />,
}
: null
}
onChange={(value) => {
updateElementData(selectedBlock, selectedElement, { dataValue: value.id, label: value.label });
}}
dropDownHeader={"RT-Data-Value"}
/>
</div>
</div>
</>
)}
{/* Data Mapping */}
{element?.type === "graph" && (
<div className="data-mapping">
<div className="heading">Data Mapping</div>
<div className="type-switch">
<div className={`type ${selectDataMapping === "singleMachine" ? "active" : ""}`} onClick={() => handleDataTypeSwitch("singleMachine")}>
Single Machine
</div>
<div className={`type ${selectDataMapping === "multipleMachine" ? "active" : ""}`} onClick={() => handleDataTypeSwitch("multipleMachine")}>
Multiple Machine
</div>
</div>
{selectDataMapping === "singleMachine" && element.dataBinding?.dataType === "single-machine" && (
<div className="fields-wrapper design-section">
{singleSourceFields.map((field) => (
<DataSourceSelector
key={field.id}
label={field.label}
options={field.options}
selected={getEventByModelUuid(selectedProduct.productUuid, element.dataBinding?.dataSource as string)?.modelName ?? ""}
onSelect={(value) => {
updateDataSource(selectedBlock, selectedElement, value.id);
}}
showEyeDropper={field.showEyeDropper}
/>
))}
{/* add delete */}
{singleValueFields.map((field, index) => (
<DataSourceSelector
key={field.id}
label={field.label}
options={field.options}
selected={field.options.find((option) => option.id === element.dataBinding?.dataValue?.[index])?.label ?? ""}
onSelect={(value) => {
const currentDataValue = Array.isArray(element.dataBinding?.dataValue)
? element.dataBinding!.dataValue
: element.dataBinding?.dataValue
? [element.dataBinding.dataValue]
: [];
const newDataValue = [...currentDataValue];
newDataValue[index] = value.id;
updateDataValue(selectedBlock, selectedElement, newDataValue);
}}
showEyeDropper={field.showEyeDropper}
showDeleteBtn={true}
onDelete={() => {
const current = Array.isArray(element.dataBinding?.dataValue)
? element.dataBinding!.dataValue
: element.dataBinding?.dataValue
? [element.dataBinding.dataValue]
: [];
const next = [...current];
next.splice(index, 1);
updateDataValue(selectedBlock, selectedElement, next);
}}
/>
))}
<div className="add-field" onClick={addField}>
<div className="icon">
<AddIcon />
</div>
<div className="label">Add Field</div>
</div>
</div>
)}
{selectDataMapping === "multipleMachine" && element.dataBinding?.dataType === "multiple-machine" && (
<div className="fields-wrapper design-section">
{multipleValueFields.map((field) => (
<DataSourceSelector
key={field.id}
label={field.label}
options={field.options}
selected={field.options.find((o) => o.id === element.dataBinding?.commonValue)?.label ?? ""}
onSelect={(value) => {
updateCommonValue(selectedBlock, selectedElement, value.id);
}}
showEyeDropper={field.showEyeDropper}
/>
))}
{/* add delete */}
{multipleSourceFields.map((field, index) => (
<DataSourceSelector
key={field.id}
label={field.label}
options={field.options}
selected={getEventByModelUuid(selectedProduct.productUuid, (element.dataBinding?.dataSource as string[])?.[index] || "")?.modelName ?? ""}
onSelect={(value) => {
const current = Array.isArray(element.dataBinding?.dataSource)
? element.dataBinding!.dataSource
: element.dataBinding?.dataSource
? [element.dataBinding.dataSource]
: [];
const next = [...current];
next[index] = value.id;
updateDataSource(selectedBlock, selectedElement, next);
}}
showEyeDropper={field.showEyeDropper}
showDeleteBtn={true}
onDelete={() => {
const current = Array.isArray(element.dataBinding?.dataSource)
? element.dataBinding!.dataSource
: element.dataBinding?.dataSource
? [element.dataBinding.dataSource]
: [];
const next = [...current];
next.splice(index, 1);
updateDataSource(selectedBlock, selectedElement, next);
}}
/>
))}
<div className="add-field" onClick={addField}>
<div className="icon">
<AddIcon />
</div>
<div className="label">Add Field</div>
</div>
</div>
)}
</div>
)}
</div>
);
};
export default ElementData;

View File

@@ -1,14 +1,10 @@
import { useCallback, useMemo, useState, useRef, useEffect, type RefObject } from "react";
import { ExtendedCSSProperties, UIElement } from "../../../../types/exportedTypes";
import { DeleteIcon } from "../../../icons/ContextMenuIcons";
import DataSourceSelector from "../../../ui/inputs/DataSourceSelector";
import InputWithDropDown from "../../../ui/inputs/InputWithDropDown";
import InputRange from "../../../ui/inputs/InputRange";
import RenameInput from "../../../ui/inputs/RenameInput";
import { AddIcon, DeviceIcon, ParametersIcon, ResizeHeightIcon } from "../../../icons/ExportCommonIcons";
import DataDetailedDropdown from "../../../ui/inputs/DataDetailedDropdown";
import { DeviceIcon, ParametersIcon, ResizeHeightIcon } from "../../../icons/ExportCommonIcons";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import ElementDesign from "./ElementDesign";
import ElementData from "./ElementData";
import { useVisualizationStore } from "../../../../store/visualization/useVisualizationStore";
interface ElementEditorProps {
@@ -60,7 +56,9 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
const { getElementById } = simulationDashBoardStore();
const element = getElementById(selectedBlock, selectedElement);
const [selectType, setSelectType] = useState("design");
const [selectDataMapping, setSelectDataMapping] = useState(element?.type === "graph" && element.dataBinding?.dataType === "multiple-machine" ? "multipleMachine" : "singleMachine");
const [selectDataMapping, setSelectDataMapping] = useState<"singleMachine" | "multipleMachine">(
element?.type === "graph" && element.dataBinding?.dataType === "multiple-machine" ? "multipleMachine" : "singleMachine"
);
// Use shared position from VisualizationStore
const { editorPosition, setEditorPosition } = useVisualizationStore();
@@ -573,210 +571,26 @@ const ElementEditor: React.FC<ElementEditorProps> = ({
/>
)}
{selectType === "data" && (
<div className="data-details">
{element?.type === "label-value" && (
<>
<div className="design-section">
<div className="sub-header">Data Handling</div>
<InputWithDropDown
label="Label"
type="string"
value={element.dataBinding?.label || ""}
placeholder={"Label 1"}
onChange={(value) => {
updateElementData(selectedBlock, selectedElement, { label: value });
}}
/>
<div className="data">
<DataDetailedDropdown
title="Data Source"
placeholder="Select assets"
sections={[
{
title: "Global",
items: [{ id: "global", label: "Global", icon: <DeviceIcon /> }],
},
{
title: "Assets",
items: getAssetDropdownItems(),
},
]}
value={
element.dataBinding?.dataSource
? {
id: element.dataBinding.dataSource as string,
label:
getEventByModelUuid(selectedProduct.productUuid, element.dataBinding.dataSource as string)?.modelName ??
(element.dataBinding.dataSource === "global" ? "Global" : ""),
icon: <DeviceIcon />,
}
: null
}
onChange={(value) => {
updateElementData(selectedBlock, selectedElement, { dataSource: value.id });
}}
dropDownHeader={"RT-Data"}
eyedroper={true}
/>
</div>
<div className="data">
<DataDetailedDropdown
title="Data Value"
placeholder="Select Value"
sections={getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined)}
value={
element.dataBinding?.dataValue
? {
id: element.dataBinding.dataValue as string,
label:
getLableValueDropdownItems(element.dataBinding?.dataSource as string | undefined)
.flatMap((section) => section.items)
.find((item) => item.id === element.dataBinding?.dataValue)?.label ?? "",
icon: <ParametersIcon />,
}
: null
}
onChange={(value) => {
updateElementData(selectedBlock, selectedElement, { dataValue: value.id, label: value.label });
}}
dropDownHeader={"RT-Data-Value"}
/>
</div>
</div>
</>
)}
{/* Data Mapping */}
{element?.type === "graph" && (
<div className="data-mapping">
<div className="heading">Data Mapping</div>
<div className="type-switch">
<div className={`type ${selectDataMapping === "singleMachine" ? "active" : ""}`} onClick={() => handleDataTypeSwitch("singleMachine")}>
Single Machine
</div>
<div className={`type ${selectDataMapping === "multipleMachine" ? "active" : ""}`} onClick={() => handleDataTypeSwitch("multipleMachine")}>
Multiple Machine
</div>
</div>
{selectDataMapping === "singleMachine" && element.dataBinding?.dataType === "single-machine" && (
<div className="fields-wrapper design-section">
{singleSourceFields.map((field) => (
<DataSourceSelector
key={field.id}
label={field.label}
options={field.options}
selected={getEventByModelUuid(selectedProduct.productUuid, element.dataBinding?.dataSource as string)?.modelName ?? ""}
onSelect={(value) => {
updateDataSource(selectedBlock, selectedElement, value.id);
}}
showEyeDropper={field.showEyeDropper}
/>
))}
{/* add delete */}
{singleValueFields.map((field, index) => (
<DataSourceSelector
key={field.id}
label={field.label}
options={field.options}
selected={field.options.find((option) => option.id === element.dataBinding?.dataValue?.[index])?.label ?? ""}
onSelect={(value) => {
const currentDataValue = Array.isArray(element.dataBinding?.dataValue)
? element.dataBinding!.dataValue
: element.dataBinding?.dataValue
? [element.dataBinding.dataValue]
: [];
const newDataValue = [...currentDataValue];
newDataValue[index] = value.id;
updateDataValue(selectedBlock, selectedElement, newDataValue);
}}
showEyeDropper={field.showEyeDropper}
showDeleteBtn={true}
onDelete={() => {
const current = Array.isArray(element.dataBinding?.dataValue)
? element.dataBinding!.dataValue
: element.dataBinding?.dataValue
? [element.dataBinding.dataValue]
: [];
const next = [...current];
next.splice(index, 1);
updateDataValue(selectedBlock, selectedElement, next);
}}
/>
))}
<div className="add-field" onClick={addField}>
<div className="icon">
<AddIcon />
</div>
<div className="label">Add Field</div>
</div>
</div>
)}
{selectDataMapping === "multipleMachine" && element.dataBinding?.dataType === "multiple-machine" && (
<div className="fields-wrapper design-section">
{multipleValueFields.map((field) => (
<DataSourceSelector
key={field.id}
label={field.label}
options={field.options}
selected={field.options.find((o) => o.id === element.dataBinding?.commonValue)?.label ?? ""}
onSelect={(value) => {
updateCommonValue(selectedBlock, selectedElement, value.id);
}}
showEyeDropper={field.showEyeDropper}
/>
))}
{/* add delete */}
{multipleSourceFields.map((field, index) => (
<DataSourceSelector
key={field.id}
label={field.label}
options={field.options}
selected={getEventByModelUuid(selectedProduct.productUuid, (element.dataBinding?.dataSource as string[])?.[index] || "")?.modelName ?? ""}
onSelect={(value) => {
const current = Array.isArray(element.dataBinding?.dataSource)
? element.dataBinding!.dataSource
: element.dataBinding?.dataSource
? [element.dataBinding.dataSource]
: [];
const next = [...current];
next[index] = value.id;
updateDataSource(selectedBlock, selectedElement, next);
}}
showEyeDropper={field.showEyeDropper}
showDeleteBtn={true}
onDelete={() => {
const current = Array.isArray(element.dataBinding?.dataSource)
? element.dataBinding!.dataSource
: element.dataBinding?.dataSource
? [element.dataBinding.dataSource]
: [];
const next = [...current];
next.splice(index, 1);
updateDataSource(selectedBlock, selectedElement, next);
}}
/>
))}
<div className="add-field" onClick={addField}>
<div className="icon">
<AddIcon />
</div>
<div className="label">Add Field</div>
</div>
</div>
)}
</div>
)}
</div>
<ElementData
element={element || null}
selectedBlock={selectedBlock}
selectedElement={selectedElement}
updateElementData={updateElementData}
selectedProduct={selectedProduct}
getEventByModelUuid={getEventByModelUuid}
getAssetDropdownItems={getAssetDropdownItems}
getLableValueDropdownItems={getLableValueDropdownItems}
singleSourceFields={singleSourceFields}
singleValueFields={singleValueFields}
multipleSourceFields={multipleSourceFields}
multipleValueFields={multipleValueFields}
selectDataMapping={selectDataMapping}
handleDataTypeSwitch={handleDataTypeSwitch}
updateDataSource={updateDataSource}
updateDataValue={updateDataValue}
updateCommonValue={updateCommonValue}
addField={addField}
/>
)}
</div>
);

View File

@@ -23,7 +23,7 @@ function Models({ loader }: { readonly loader: GLTFLoader }) {
const { controls, camera } = useThree();
const assetGroupRef = useRef<Group>(null);
const { assetStore, layout } = useSceneContext();
const { assets, selectedAssets, getSelectedAssetUuids } = assetStore();
const { assets, selectedAssets, getSelectedAssetUuids, clearSelectedAssets } = assetStore();
const { selectedAsset, clearSelectedAsset } = useSelectedAsset();
const { contextAction, setContextAction } = useContextActionStore();
const { limitDistance } = useLimitDistance();
@@ -126,6 +126,7 @@ function Models({ loader }: { readonly loader: GLTFLoader }) {
if (selectedAssets.length > 0) {
const target = (controls as CameraControls).getTarget(new Vector3());
(controls as CameraControls).setTarget(target.x, 0, target.z, true);
clearSelectedAssets();
}
if (selectedAsset) {
clearSelectedAsset();