Enhance AssetOutline component with group selection and multi-selection features; refactor related styles and APIs for improved functionality
This commit is contained in:
@@ -1,15 +1,17 @@
|
|||||||
import { useState, useRef, DragEvent, useCallback } from "react";
|
import { useState, useRef, DragEvent, useCallback, useMemo } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { EyeIcon, LockIcon, FolderIcon, ChevronIcon, CubeIcon, AddIcon, KebebIcon, CollapseAllIcon, FocusIcon } from "../../../icons/ExportCommonIcons";
|
import { EyeIcon, LockIcon, FolderIcon, ChevronIcon, CubeIcon, AddIcon, KebebIcon, CollapseAllIcon, FocusIcon, DeleteIcon } from "../../../icons/ExportCommonIcons";
|
||||||
import RenameInput from "../../inputs/RenameInput";
|
import RenameInput from "../../inputs/RenameInput";
|
||||||
|
import clsx from "clsx";
|
||||||
import { useSceneContext } from "../../../../modules/scene/sceneContext";
|
import { useSceneContext } from "../../../../modules/scene/sceneContext";
|
||||||
import { useSocketStore } from "../../../../store/socket/useSocketStore";
|
import { useSocketStore } from "../../../../store/socket/useSocketStore";
|
||||||
import useAssetResponseHandler from "../../../../modules/collaboration/responseHandler/useAssetResponseHandler";
|
import useAssetResponseHandler from "../../../../modules/collaboration/responseHandler/useAssetResponseHandler";
|
||||||
|
import useZoomMesh from "../../../../modules/builder/hooks/useZoomMesh";
|
||||||
|
|
||||||
import { getUserData } from "../../../../functions/getUserData";
|
import { getUserData } from "../../../../functions/getUserData";
|
||||||
|
import { useOuterClick } from "../../../../utils/useOuterClick";
|
||||||
|
|
||||||
import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
|
import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
|
||||||
import clsx from "clsx";
|
|
||||||
import { useContextActionStore } from "../../../../store/builder/store";
|
|
||||||
|
|
||||||
interface DragState {
|
interface DragState {
|
||||||
draggedItem: AssetGroupChild | null;
|
draggedItem: AssetGroupChild | null;
|
||||||
@@ -18,7 +20,7 @@ interface DragState {
|
|||||||
isRootTarget: boolean;
|
isRootTarget: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tree Node Component
|
// Enhanced Tree Node Component with Group Selection
|
||||||
const TreeNode = ({
|
const TreeNode = ({
|
||||||
item,
|
item,
|
||||||
level = 0,
|
level = 0,
|
||||||
@@ -44,7 +46,7 @@ const TreeNode = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { assetGroupStore, assetStore } = useSceneContext();
|
const { assetGroupStore, assetStore } = useSceneContext();
|
||||||
const { hasSelectedAsset, selectedAssets } = assetStore();
|
const { hasSelectedAsset, selectedAssets } = assetStore();
|
||||||
const { isGroup, getGroupsContainingAsset, getGroupsContainingGroup } = assetGroupStore();
|
const { isGroup, hasSelectedGroup, selectedGroups, getParentGroup } = assetGroupStore();
|
||||||
const isGroupNode = isGroup(item);
|
const isGroupNode = isGroup(item);
|
||||||
|
|
||||||
const itemId = isGroupNode ? item.groupUuid : item.modelUuid;
|
const itemId = isGroupNode ? item.groupUuid : item.modelUuid;
|
||||||
@@ -52,31 +54,26 @@ const TreeNode = ({
|
|||||||
const isVisible = item.isVisible;
|
const isVisible = item.isVisible;
|
||||||
const isLocked = item.isLocked;
|
const isLocked = item.isLocked;
|
||||||
const isExpanded = isGroupNode ? item.isExpanded : false;
|
const isExpanded = isGroupNode ? item.isExpanded : false;
|
||||||
const isSelected = !isGroupNode ? hasSelectedAsset(item.modelUuid) : false;
|
|
||||||
const isMultiSelected = !isGroupNode && selectedAssets.length > 1 && hasSelectedAsset(item.modelUuid);
|
|
||||||
|
|
||||||
// Determine the parent group of this item
|
const isSelected = isGroupNode ? hasSelectedGroup(item.groupUuid) : hasSelectedAsset(item.modelUuid);
|
||||||
const getParentGroup = useCallback(
|
|
||||||
(currentItem: AssetGroupChild): string | null => {
|
const getMultiSelectionState = (item: AssetGroupChild) => {
|
||||||
if (isGroup(currentItem)) {
|
const totalSelectedItems = selectedGroups.length + selectedAssets.length;
|
||||||
const parents = getGroupsContainingGroup(currentItem.groupUuid);
|
|
||||||
return parents.length > 0 ? parents[0].groupUuid : null;
|
if (totalSelectedItems <= 1) return false;
|
||||||
} else {
|
|
||||||
const parents = getGroupsContainingAsset(currentItem.modelUuid);
|
if (isGroup(item)) {
|
||||||
return parents.length > 0 ? parents[0].groupUuid : null;
|
return selectedGroups.includes(item.groupUuid);
|
||||||
}
|
} else {
|
||||||
},
|
return hasSelectedAsset(item.modelUuid);
|
||||||
[getGroupsContainingAsset, getGroupsContainingGroup, isGroup]
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const isMultiSelected = getMultiSelectionState(item);
|
||||||
|
|
||||||
// Check if this node should be highlighted as a drop target
|
|
||||||
const isDropTarget = useCallback(() => {
|
const isDropTarget = useCallback(() => {
|
||||||
if (!dragState.draggedItem || !dragState.targetGroupUuid || !isGroupNode) return false;
|
if (!dragState.draggedItem || !dragState.targetGroupUuid || !isGroupNode) return false;
|
||||||
|
|
||||||
// Get the group UUID this item belongs to or is
|
|
||||||
const thisGroupUuid = item.groupUuid;
|
const thisGroupUuid = item.groupUuid;
|
||||||
|
|
||||||
// Highlight if this is the target group or belongs to the target group
|
|
||||||
return thisGroupUuid === dragState.targetGroupUuid;
|
return thisGroupUuid === dragState.targetGroupUuid;
|
||||||
}, [dragState, isGroupNode, item]);
|
}, [dragState, isGroupNode, item]);
|
||||||
|
|
||||||
@@ -112,9 +109,11 @@ const TreeNode = ({
|
|||||||
const shouldShowHighlight = isDropTarget();
|
const shouldShowHighlight = isDropTarget();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={itemId} className={`tree-node ${shouldShowHighlight ? "drop-target-highlight" : ""}`}>
|
<div key={itemId} className={`tree-node ${shouldShowHighlight ? "drop-target-highlight" : ""} ${isGroupNode && isSelected ? "group-selected" : ""}`}>
|
||||||
<div
|
<div
|
||||||
className={clsx("tree-node-content", {
|
className={clsx("tree-node-content", {
|
||||||
|
"group-node": isGroupNode,
|
||||||
|
"asset-node": !isGroupNode,
|
||||||
locked: isLocked,
|
locked: isLocked,
|
||||||
hidden: !isVisible,
|
hidden: !isVisible,
|
||||||
dragging: dragState.draggedItem === item,
|
dragging: dragState.draggedItem === item,
|
||||||
@@ -140,40 +139,19 @@ const TreeNode = ({
|
|||||||
<RenameInput value={itemName} onRename={() => {}} canEdit={true} />
|
<RenameInput value={itemName} onRename={() => {}} canEdit={true} />
|
||||||
|
|
||||||
<div className="node-controls">
|
<div className="node-controls">
|
||||||
<button
|
<button className="control-button" title={isVisible ? "Visible" : "Hidden"} onClick={(e) => handleOptionClick(e, "visibility")}>
|
||||||
className="control-button"
|
|
||||||
title={isVisible ? "Visible" : "Hidden"}
|
|
||||||
onClick={(e) => {
|
|
||||||
handleOptionClick(e, "visibility");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EyeIcon isClosed={!isVisible} />
|
<EyeIcon isClosed={!isVisible} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
{isGroupNode && item.children && item.children.length > 0 && (
|
||||||
className="control-button"
|
<button className="control-button" title="Focus" onClick={(e) => handleOptionClick(e, "focus")}>
|
||||||
title={isLocked ? "Locked" : "Unlocked"}
|
<FocusIcon />
|
||||||
onClick={(e) => {
|
</button>
|
||||||
handleOptionClick(e, "focus");
|
)}
|
||||||
}}
|
<button className="control-button" title={isLocked ? "Locked" : "Unlocked"} onClick={(e) => handleOptionClick(e, "lock")}>
|
||||||
>
|
|
||||||
<FocusIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="control-button"
|
|
||||||
title={isLocked ? "Locked" : "Unlocked"}
|
|
||||||
onClick={(e) => {
|
|
||||||
handleOptionClick(e, "lock");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LockIcon isLocked={isLocked} />
|
<LockIcon isLocked={isLocked} />
|
||||||
</button>
|
</button>
|
||||||
{isGroupNode && (
|
{isGroupNode && (
|
||||||
<button
|
<button className="control-button" onClick={(e) => handleOptionClick(e, "kebab")}>
|
||||||
className="control-button"
|
|
||||||
onClick={(e) => {
|
|
||||||
handleOptionClick(e, "kebab");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<KebebIcon />
|
<KebebIcon />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -181,14 +159,7 @@ const TreeNode = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isGroupNode && isExpanded && item.children && (
|
{isGroupNode && isExpanded && item.children && (
|
||||||
<div
|
<div className="tree-children" style={{ "--left": level } as React.CSSProperties}>
|
||||||
className="tree-children"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--left": level,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item.children.map((child) => (
|
{item.children.map((child) => (
|
||||||
<TreeNode
|
<TreeNode
|
||||||
key={isGroup(child) ? child.groupUuid : child.modelUuid}
|
key={isGroup(child) ? child.groupUuid : child.modelUuid}
|
||||||
@@ -213,7 +184,6 @@ const TreeNode = ({
|
|||||||
// Main Component
|
// Main Component
|
||||||
export const AssetOutline = () => {
|
export const AssetOutline = () => {
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
const { setContextAction } = useContextActionStore();
|
|
||||||
const lastSelectedRef = useRef<{ item: AssetGroupChild; index: number } | null>(null);
|
const lastSelectedRef = useRef<{ item: AssetGroupChild; index: number } | null>(null);
|
||||||
const dragStateRef = useRef<DragState>({
|
const dragStateRef = useRef<DragState>({
|
||||||
draggedItem: null,
|
draggedItem: null,
|
||||||
@@ -223,14 +193,30 @@ export const AssetOutline = () => {
|
|||||||
});
|
});
|
||||||
const [_, forceUpdate] = useState({});
|
const [_, forceUpdate] = useState({});
|
||||||
const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext();
|
const { scene, assetGroupStore, assetStore, versionStore, undoRedo3DStore } = useSceneContext();
|
||||||
const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, peekToggleLock, toggleSelectedAsset, selectedAssets } = assetStore();
|
const { addSelectedAsset, clearSelectedAssets, getAssetById, peekToggleVisibility, peekToggleLock, toggleSelectedAsset, selectedAssets, setSelectedAssets } = assetStore();
|
||||||
const { groupHierarchy, isGroup, getGroupsContainingAsset, getFlatGroupChildren, setGroupExpanded, addChildToGroup, removeChildFromGroup, getGroupsContainingGroup } = assetGroupStore();
|
const {
|
||||||
|
groupHierarchy,
|
||||||
|
isGroup,
|
||||||
|
getGroupsContainingAsset,
|
||||||
|
getFlatGroupChildren,
|
||||||
|
setGroupExpanded,
|
||||||
|
addChildToGroup,
|
||||||
|
removeChildFromGroup,
|
||||||
|
getGroupsContainingGroup,
|
||||||
|
selectedGroups,
|
||||||
|
addSelectedGroup,
|
||||||
|
removeSelectedGroup,
|
||||||
|
toggleSelectedGroup,
|
||||||
|
clearSelectedGroups,
|
||||||
|
hasSelectedGroup,
|
||||||
|
} = assetGroupStore();
|
||||||
const { projectId } = useParams();
|
const { projectId } = useParams();
|
||||||
const { push3D } = undoRedo3DStore();
|
const { push3D } = undoRedo3DStore();
|
||||||
const { builderSocket } = useSocketStore();
|
const { builderSocket } = useSocketStore();
|
||||||
const { userId, organization } = getUserData();
|
const { userId, organization } = getUserData();
|
||||||
const { selectedVersion } = versionStore();
|
const { selectedVersion } = versionStore();
|
||||||
const { updateAssetInScene } = useAssetResponseHandler();
|
const { updateAssetInScene } = useAssetResponseHandler();
|
||||||
|
const { zoomMeshes } = useZoomMesh();
|
||||||
|
|
||||||
const getFlattenedHierarchy = useCallback((): AssetGroupChild[] => {
|
const getFlattenedHierarchy = useCallback((): AssetGroupChild[] => {
|
||||||
const flattened: AssetGroupChild[] = [];
|
const flattened: AssetGroupChild[] = [];
|
||||||
@@ -568,7 +554,7 @@ export const AssetOutline = () => {
|
|||||||
(e: React.MouseEvent, item: AssetGroupChild) => {
|
(e: React.MouseEvent, item: AssetGroupChild) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const isShiftClick = e.shiftKey;
|
const isShiftClick = e.shiftKey;
|
||||||
const isCtrlClick = e.ctrlKey;
|
const isCtrlClick = e.ctrlKey || e.metaKey;
|
||||||
|
|
||||||
if (!scene.current) return;
|
if (!scene.current) return;
|
||||||
|
|
||||||
@@ -577,45 +563,55 @@ export const AssetOutline = () => {
|
|||||||
return isGroup(i) ? i.groupUuid : i.modelUuid;
|
return isGroup(i) ? i.groupUuid : i.modelUuid;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const itemId = getItemId(item);
|
||||||
|
const flattened = getFlattenedHierarchy();
|
||||||
|
const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === itemId);
|
||||||
|
|
||||||
// Single click (no modifiers) - select only this item
|
// Single click (no modifiers) - select only this item
|
||||||
if (!isShiftClick && !isCtrlClick) {
|
if (!isShiftClick && !isCtrlClick) {
|
||||||
if (!isGroup(item)) {
|
if (isGroup(item)) {
|
||||||
|
clearSelectedAssets();
|
||||||
|
clearSelectedGroups();
|
||||||
|
addSelectedGroup(item.groupUuid);
|
||||||
|
lastSelectedRef.current = { item, index: clickedIndex };
|
||||||
|
} else {
|
||||||
|
clearSelectedGroups();
|
||||||
|
clearSelectedAssets();
|
||||||
const asset = scene.current.getObjectByProperty("uuid", item.modelUuid);
|
const asset = scene.current.getObjectByProperty("uuid", item.modelUuid);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
clearSelectedAssets();
|
|
||||||
addSelectedAsset(asset);
|
addSelectedAsset(asset);
|
||||||
|
lastSelectedRef.current = { item, index: clickedIndex };
|
||||||
// Update last selected reference
|
|
||||||
const flattened = getFlattenedHierarchy();
|
|
||||||
const index = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item));
|
|
||||||
lastSelectedRef.current = { item, index };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl+Click - toggle selection
|
// Ctrl+Click - toggle selection
|
||||||
else if (isCtrlClick && !isShiftClick) {
|
else if (isCtrlClick && !isShiftClick) {
|
||||||
if (!isGroup(item)) {
|
if (isGroup(item)) {
|
||||||
|
toggleSelectedGroup(item.groupUuid);
|
||||||
|
lastSelectedRef.current = { item, index: clickedIndex };
|
||||||
|
} else {
|
||||||
const asset = scene.current.getObjectByProperty("uuid", item.modelUuid);
|
const asset = scene.current.getObjectByProperty("uuid", item.modelUuid);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
toggleSelectedAsset(asset);
|
toggleSelectedAsset(asset);
|
||||||
|
lastSelectedRef.current = { item, index: clickedIndex };
|
||||||
// Update last selected reference
|
|
||||||
const flattened = getFlattenedHierarchy();
|
|
||||||
const index = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item));
|
|
||||||
lastSelectedRef.current = { item, index };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Shift+Click - range selection
|
// Shift+Click - range selection
|
||||||
else if (isShiftClick) {
|
else if (isShiftClick) {
|
||||||
const flattened = getFlattenedHierarchy();
|
|
||||||
const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === getItemId(item));
|
|
||||||
|
|
||||||
if (clickedIndex === -1) return;
|
if (clickedIndex === -1) return;
|
||||||
|
|
||||||
// If no last selected item, treat as normal selection
|
// If no last selected item, treat as normal selection
|
||||||
if (!lastSelectedRef.current) {
|
if (!lastSelectedRef.current) {
|
||||||
if (!isGroup(item)) {
|
if (isGroup(item)) {
|
||||||
|
clearSelectedAssets();
|
||||||
|
clearSelectedGroups();
|
||||||
|
addSelectedGroup(item.groupUuid);
|
||||||
|
lastSelectedRef.current = { item, index: clickedIndex };
|
||||||
|
} else {
|
||||||
|
clearSelectedGroups();
|
||||||
|
clearSelectedAssets();
|
||||||
const asset = scene.current.getObjectByProperty("uuid", item.modelUuid);
|
const asset = scene.current.getObjectByProperty("uuid", item.modelUuid);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
clearSelectedAssets();
|
clearSelectedAssets();
|
||||||
@@ -633,20 +629,29 @@ export const AssetOutline = () => {
|
|||||||
// Get all items in range
|
// Get all items in range
|
||||||
const itemsInRange = flattened.slice(startIndex, endIndex + 1);
|
const itemsInRange = flattened.slice(startIndex, endIndex + 1);
|
||||||
|
|
||||||
// Filter out groups, only select assets
|
const groupsInRange: AssetGroupHierarchyNode[] = [];
|
||||||
const assetsInRange: Asset[] = [];
|
const assetsInRange: Asset[] = [];
|
||||||
|
|
||||||
itemsInRange.forEach((rangeItem) => {
|
itemsInRange.forEach((rangeItem) => {
|
||||||
if (!isGroup(rangeItem)) {
|
if (isGroup(rangeItem)) {
|
||||||
|
groupsInRange.push(rangeItem);
|
||||||
|
} else {
|
||||||
assetsInRange.push(rangeItem);
|
assetsInRange.push(rangeItem);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If Ctrl+Shift, add to existing selection; otherwise replace
|
// If Ctrl+Shift, add to existing selection; otherwise replace
|
||||||
if (!isCtrlClick) {
|
if (!isCtrlClick) {
|
||||||
|
clearSelectedGroups();
|
||||||
clearSelectedAssets();
|
clearSelectedAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all assets in range to selection
|
groupsInRange.forEach((group) => {
|
||||||
|
if (!hasSelectedGroup(group.groupUuid)) {
|
||||||
|
addSelectedGroup(group.groupUuid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
assetsInRange.forEach((assetItem) => {
|
assetsInRange.forEach((assetItem) => {
|
||||||
const asset = scene.current!.getObjectByProperty("uuid", assetItem.modelUuid);
|
const asset = scene.current!.getObjectByProperty("uuid", assetItem.modelUuid);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
@@ -655,11 +660,28 @@ export const AssetOutline = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[scene, isGroup, clearSelectedAssets, addSelectedAsset, getFlattenedHierarchy, toggleSelectedAsset]
|
[
|
||||||
|
scene,
|
||||||
|
isGroup,
|
||||||
|
clearSelectedAssets,
|
||||||
|
addSelectedAsset,
|
||||||
|
getFlattenedHierarchy,
|
||||||
|
toggleSelectedAsset,
|
||||||
|
toggleSelectedGroup,
|
||||||
|
clearSelectedGroups,
|
||||||
|
addSelectedGroup,
|
||||||
|
removeSelectedGroup,
|
||||||
|
selectedGroups,
|
||||||
|
selectedAssets,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOptionClick = useCallback(
|
const handleOptionClick = useCallback(
|
||||||
(option: string, item: AssetGroupChild) => {
|
(option: string, item: AssetGroupChild) => {
|
||||||
|
const getItemId = (i: AssetGroupChild): string => {
|
||||||
|
return isGroup(i) ? i.groupUuid : i.modelUuid;
|
||||||
|
};
|
||||||
|
|
||||||
if (option === "visibility") {
|
if (option === "visibility") {
|
||||||
if (isGroup(item)) {
|
if (isGroup(item)) {
|
||||||
} else {
|
} else {
|
||||||
@@ -747,7 +769,17 @@ export const AssetOutline = () => {
|
|||||||
} else if (option === "focus") {
|
} else if (option === "focus") {
|
||||||
if (isGroup(item)) {
|
if (isGroup(item)) {
|
||||||
} else {
|
} else {
|
||||||
setContextAction("focusAsset");
|
clearSelectedGroups();
|
||||||
|
clearSelectedAssets();
|
||||||
|
const asset = scene.current?.getObjectByProperty("uuid", item.modelUuid);
|
||||||
|
if (asset) {
|
||||||
|
const itemId = getItemId(item);
|
||||||
|
const flattened = getFlattenedHierarchy();
|
||||||
|
const clickedIndex = flattened.findIndex((flatItem) => getItemId(flatItem) === itemId);
|
||||||
|
addSelectedAsset(asset);
|
||||||
|
lastSelectedRef.current = { item, index: clickedIndex };
|
||||||
|
zoomMeshes([itemId]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (option === "kebab") {
|
} else if (option === "kebab") {
|
||||||
if (isGroup(item)) {
|
if (isGroup(item)) {
|
||||||
@@ -758,6 +790,30 @@ export const AssetOutline = () => {
|
|||||||
[selectedVersion, builderSocket, projectId, userId, organization]
|
[selectedVersion, builderSocket, projectId, userId, organization]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleAddGroup = useCallback(() => {}, [assetGroupStore, clearSelectedGroups, addSelectedGroup]);
|
||||||
|
|
||||||
|
const handleExpandAll = useCallback(() => {}, [assetGroupStore, setGroupExpanded]);
|
||||||
|
|
||||||
|
// Selection statistics
|
||||||
|
const selectionStats = useMemo(() => {
|
||||||
|
const totalSelectedAssets = selectedAssets.length;
|
||||||
|
const totalSelectedGroups = selectedGroups.length;
|
||||||
|
|
||||||
|
if (totalSelectedGroups > 0 && totalSelectedAssets > 0) {
|
||||||
|
return `${totalSelectedGroups} group${totalSelectedGroups > 1 ? "s" : ""} and ${totalSelectedAssets} asset${totalSelectedAssets > 1 ? "s" : ""} selected`;
|
||||||
|
} else if (totalSelectedGroups > 0) {
|
||||||
|
return `${totalSelectedGroups} group${totalSelectedGroups > 1 ? "s" : ""} selected`;
|
||||||
|
} else if (totalSelectedAssets > 0) {
|
||||||
|
return `${totalSelectedAssets} asset${totalSelectedAssets > 1 ? "s" : ""} selected`;
|
||||||
|
} else {
|
||||||
|
return `${groupHierarchy.length} root items`;
|
||||||
|
}
|
||||||
|
}, [selectedAssets.length, selectedGroups.length, groupHierarchy.length]);
|
||||||
|
|
||||||
|
useOuterClick(() => {
|
||||||
|
clearSelectedGroups();
|
||||||
|
}, ["tree-node"]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="outline-overlay" onDragEnd={handleDragEnd}>
|
<div className="outline-overlay" onDragEnd={handleDragEnd}>
|
||||||
@@ -767,24 +823,14 @@ export const AssetOutline = () => {
|
|||||||
<p>Assets</p>
|
<p>Assets</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="outline-toolbar">
|
<div className="outline-toolbar">
|
||||||
<button className="toolbar-button" title="Add Group">
|
<button className="toolbar-button" title="Add Group" onClick={handleAddGroup}>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
</button>
|
</button>
|
||||||
{/* <button className="toolbar-button" title="Delete">
|
<button className="toolbar-button" title="Expand All" onClick={handleExpandAll}>
|
||||||
<DeleteIcon />
|
|
||||||
</button> */}
|
|
||||||
<button
|
|
||||||
className="toolbar-button"
|
|
||||||
title="Expand All"
|
|
||||||
onClick={() => {
|
|
||||||
const { assetGroups, setGroupExpanded } = assetGroupStore.getState();
|
|
||||||
assetGroups.forEach((group) => setGroupExpanded(group.groupUuid, true));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CollapseAllIcon />
|
<CollapseAllIcon />
|
||||||
</button>
|
</button>
|
||||||
<button className="close-button" onClick={() => setIsOpen(!isOpen)}>
|
<button className="close-button" onClick={() => setIsOpen(!isOpen)}>
|
||||||
<ChevronIcon isOpen />
|
<ChevronIcon isOpen={isOpen} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -810,9 +856,7 @@ export const AssetOutline = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="outline-footer">
|
<div className="outline-footer">
|
||||||
<span className={`footer-stats ${selectedAssets.length > 1 ? "multi-selection" : ""}`}>
|
<span className={`footer-stats ${selectedAssets.length + selectedGroups.length > 1 ? "multi-selection" : ""}`}>{selectionStats}</span>
|
||||||
{selectedAssets.length > 1 ? `${selectedAssets.length} items selected` : `${groupHierarchy.length} root items`}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function WallCreator() {
|
|||||||
|
|
||||||
const addWallToBackend = (wall: Wall) => {
|
const addWallToBackend = (wall: Wall) => {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
if (builderSocket?.connected) {
|
if (!builderSocket?.connected) {
|
||||||
// API
|
// API
|
||||||
|
|
||||||
upsertWallApi(projectId, selectedVersion?.versionId || "", wall)
|
upsertWallApi(projectId, selectedVersion?.versionId || "", wall)
|
||||||
@@ -104,7 +104,7 @@ function WallCreator() {
|
|||||||
organization: organization,
|
organization: organization,
|
||||||
};
|
};
|
||||||
|
|
||||||
// builderSocket.emit("v1:model-Wall:add", data);
|
builderSocket.emit("v1:model-Wall:add", data);
|
||||||
|
|
||||||
setTempPoints([]);
|
setTempPoints([]);
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ function GroupControls() {
|
|||||||
if (!projectId || !selectedVersion) return;
|
if (!projectId || !selectedVersion) return;
|
||||||
|
|
||||||
getAssetGroupsApi(projectId, selectedVersion.versionId).then((data) => {
|
getAssetGroupsApi(projectId, selectedVersion.versionId).then((data) => {
|
||||||
console.log("data: ", data);
|
|
||||||
if (data && data.length > 0) {
|
if (data && data.length > 0) {
|
||||||
setGroups(data);
|
setGroups(data);
|
||||||
}
|
}
|
||||||
@@ -61,25 +60,24 @@ function GroupControls() {
|
|||||||
isVisible: true,
|
isVisible: true,
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
childrens: assetUuids.map((assetUuid) => {
|
children: assetUuids.map((assetUuid) => {
|
||||||
return { type: "Asset", childrenUuid: assetUuid };
|
return { type: "Asset", childrenUuid: assetUuid };
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
console.log("assetGroup: ", assetGroup);
|
|
||||||
// addGroup(assetGroup);
|
|
||||||
|
|
||||||
createAssetGroupApi({
|
// createAssetGroupApi({
|
||||||
projectId,
|
// projectId,
|
||||||
versionId: selectedVersion.versionId,
|
// versionId: selectedVersion.versionId,
|
||||||
groupUuid: assetGroup.groupUuid,
|
// groupUuid: assetGroup.groupUuid,
|
||||||
groupName: assetGroup.groupName,
|
// groupName: assetGroup.groupName,
|
||||||
isVisible: assetGroup.isVisible,
|
// isVisible: assetGroup.isVisible,
|
||||||
isExpanded: assetGroup.isExpanded,
|
// isExpanded: assetGroup.isExpanded,
|
||||||
isLocked: assetGroup.isLocked,
|
// isLocked: assetGroup.isLocked,
|
||||||
childrens: assetGroup.childrens,
|
// children: assetGroup.children,
|
||||||
}).then((data) => {
|
// }).then((data) => {
|
||||||
console.log("data: ", data);
|
// console.log("data: ", data);
|
||||||
});
|
// });
|
||||||
|
addGroup(assetGroup);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const createAssetGroupApi = async ({
|
|||||||
isVisible,
|
isVisible,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
isLocked,
|
isLocked,
|
||||||
childrens,
|
children,
|
||||||
}: {
|
}: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
versionId: string;
|
versionId: string;
|
||||||
@@ -17,7 +17,7 @@ export const createAssetGroupApi = async ({
|
|||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
childrens: {
|
children: {
|
||||||
type: "Asset" | "Group";
|
type: "Asset" | "Group";
|
||||||
childrenUuid: string;
|
childrenUuid: string;
|
||||||
}[];
|
}[];
|
||||||
@@ -37,7 +37,7 @@ export const createAssetGroupApi = async ({
|
|||||||
isVisible: isVisible,
|
isVisible: isVisible,
|
||||||
isExpanded: isExpanded,
|
isExpanded: isExpanded,
|
||||||
isLocked: isLocked,
|
isLocked: isLocked,
|
||||||
childrens: childrens,
|
children: children,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ interface AssetGroupStore {
|
|||||||
getGroupById: (groupUuid: string) => AssetGroup | undefined;
|
getGroupById: (groupUuid: string) => AssetGroup | undefined;
|
||||||
getGroupsContainingAsset: (assetUuid: string) => AssetGroup[];
|
getGroupsContainingAsset: (assetUuid: string) => AssetGroup[];
|
||||||
getGroupsContainingGroup: (childGroupUuid: string) => AssetGroup[];
|
getGroupsContainingGroup: (childGroupUuid: string) => AssetGroup[];
|
||||||
|
getParentGroup: (item: AssetGroupChild) => string | null;
|
||||||
hasGroup: (groupUuid: string) => boolean;
|
hasGroup: (groupUuid: string) => boolean;
|
||||||
isGroup: (item: AssetGroupChild) => item is AssetGroupHierarchyNode;
|
isGroup: (item: AssetGroupChild) => item is AssetGroupHierarchyNode;
|
||||||
isEmptyGroup: (groupUuid: string) => boolean;
|
isEmptyGroup: (groupUuid: string) => boolean;
|
||||||
@@ -69,18 +70,18 @@ export const createAssetGroupStore = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find all asset children in the new group
|
// Find all asset children in the new group
|
||||||
const assetChildren = group.childrens.filter((child) => child.type === "Asset");
|
const assetChildren = group.children.filter((child) => child.type === "Asset");
|
||||||
const assetUuids = new Set(assetChildren.map((child) => child.childrenUuid));
|
const assetUuids = new Set(assetChildren.map((child) => child.childrenUuid));
|
||||||
|
|
||||||
// Remove these assets from existing groups and track updated groups
|
// Remove these assets from existing groups and track updated groups
|
||||||
const updatedGroups: AssetGroup[] = [];
|
const updatedGroups: AssetGroup[] = [];
|
||||||
|
|
||||||
state.assetGroups.forEach((existingGroup) => {
|
state.assetGroups.forEach((existingGroup) => {
|
||||||
const originalLength = existingGroup.childrens.length;
|
const originalLength = existingGroup.children.length;
|
||||||
existingGroup.childrens = existingGroup.childrens.filter((child) => !(child.type === "Asset" && assetUuids.has(child.childrenUuid)));
|
existingGroup.children = existingGroup.children.filter((child) => !(child.type === "Asset" && assetUuids.has(child.childrenUuid)));
|
||||||
|
|
||||||
// If group was modified, add to updated groups
|
// If group was modified, add to updated groups
|
||||||
if (existingGroup.childrens.length !== originalLength) {
|
if (existingGroup.children.length !== originalLength) {
|
||||||
updatedGroups.push({ ...existingGroup });
|
updatedGroups.push({ ...existingGroup });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -98,7 +99,7 @@ export const createAssetGroupStore = () => {
|
|||||||
set((state) => {
|
set((state) => {
|
||||||
// First remove this group from any parent groups
|
// First remove this group from any parent groups
|
||||||
state.assetGroups.forEach((group) => {
|
state.assetGroups.forEach((group) => {
|
||||||
group.childrens = group.childrens.filter((child) => !(child.type === "Group" && child.childrenUuid === groupUuid));
|
group.children = group.children.filter((child) => !(child.type === "Group" && child.childrenUuid === groupUuid));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Then remove the group itself
|
// Then remove the group itself
|
||||||
@@ -179,17 +180,17 @@ export const createAssetGroupStore = () => {
|
|||||||
state.assetGroups.forEach((group) => {
|
state.assetGroups.forEach((group) => {
|
||||||
if (group.groupUuid === groupUuid) return; // skip target group
|
if (group.groupUuid === groupUuid) return; // skip target group
|
||||||
|
|
||||||
const originalLength = group.childrens.length;
|
const originalLength = group.children.length;
|
||||||
group.childrens = group.childrens.filter((c) => c.childrenUuid !== child.childrenUuid);
|
group.children = group.children.filter((c) => c.childrenUuid !== child.childrenUuid);
|
||||||
|
|
||||||
if (group.childrens.length !== originalLength) {
|
if (group.children.length !== originalLength) {
|
||||||
updatedGroups.push({ ...group });
|
updatedGroups.push({ ...group });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2️⃣ Add the child to the target group (if not already present)
|
// 2️⃣ Add the child to the target group (if not already present)
|
||||||
if (!targetGroup.childrens.some((c) => c.childrenUuid === child.childrenUuid)) {
|
if (!targetGroup.children.some((c) => c.childrenUuid === child.childrenUuid)) {
|
||||||
targetGroup.childrens.push(child);
|
targetGroup.children.push(child);
|
||||||
updatedGroups.push({ ...targetGroup });
|
updatedGroups.push({ ...targetGroup });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +207,7 @@ export const createAssetGroupStore = () => {
|
|||||||
set((state) => {
|
set((state) => {
|
||||||
const group = state.assetGroups.find((g) => g.groupUuid === groupUuid);
|
const group = state.assetGroups.find((g) => g.groupUuid === groupUuid);
|
||||||
if (group) {
|
if (group) {
|
||||||
group.childrens = group.childrens.filter((child) => child.childrenUuid !== childUuid);
|
group.children = group.children.filter((child) => child.childrenUuid !== childUuid);
|
||||||
state.groupHierarchy = get().buildHierarchy([], state.assetGroups);
|
state.groupHierarchy = get().buildHierarchy([], state.assetGroups);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -214,7 +215,7 @@ export const createAssetGroupStore = () => {
|
|||||||
|
|
||||||
getGroupChildren: (groupUuid) => {
|
getGroupChildren: (groupUuid) => {
|
||||||
const group = get().assetGroups.find((g) => g.groupUuid === groupUuid);
|
const group = get().assetGroups.find((g) => g.groupUuid === groupUuid);
|
||||||
return group?.childrens || [];
|
return group?.children || [];
|
||||||
},
|
},
|
||||||
|
|
||||||
// Group properties
|
// Group properties
|
||||||
@@ -271,7 +272,7 @@ export const createAssetGroupStore = () => {
|
|||||||
const buildNode = (group: AssetGroup): AssetGroupHierarchyNode => {
|
const buildNode = (group: AssetGroup): AssetGroupHierarchyNode => {
|
||||||
const children: AssetGroupChild[] = [];
|
const children: AssetGroupChild[] = [];
|
||||||
|
|
||||||
group.childrens.forEach((child) => {
|
group.children.forEach((child) => {
|
||||||
if (child.type === "Asset") {
|
if (child.type === "Asset") {
|
||||||
const asset = assetMap.get(child.childrenUuid);
|
const asset = assetMap.get(child.childrenUuid);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
@@ -300,7 +301,7 @@ export const createAssetGroupStore = () => {
|
|||||||
// Find root groups (groups that are not children of any other group)
|
// Find root groups (groups that are not children of any other group)
|
||||||
const childGroupUuids = new Set();
|
const childGroupUuids = new Set();
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
group.childrens.forEach((child) => {
|
group.children.forEach((child) => {
|
||||||
if (child.type === "Group") {
|
if (child.type === "Group") {
|
||||||
childGroupUuids.add(child.childrenUuid);
|
childGroupUuids.add(child.childrenUuid);
|
||||||
}
|
}
|
||||||
@@ -345,7 +346,7 @@ export const createAssetGroupStore = () => {
|
|||||||
isVisible: node.isVisible,
|
isVisible: node.isVisible,
|
||||||
isLocked: node.isLocked,
|
isLocked: node.isLocked,
|
||||||
isExpanded: node.isExpanded,
|
isExpanded: node.isExpanded,
|
||||||
childrens: node.children.map((child) =>
|
children: node.children.map((child) =>
|
||||||
"modelUuid" in child ? { type: "Asset" as const, childrenUuid: child.modelUuid } : { type: "Group" as const, childrenUuid: child.groupUuid }
|
"modelUuid" in child ? { type: "Asset" as const, childrenUuid: child.modelUuid } : { type: "Group" as const, childrenUuid: child.groupUuid }
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -413,13 +414,13 @@ export const createAssetGroupStore = () => {
|
|||||||
if (child.type === "Group") {
|
if (child.type === "Group") {
|
||||||
const childGroup = get().assetGroups.find((g) => g.groupUuid === child.childrenUuid);
|
const childGroup = get().assetGroups.find((g) => g.groupUuid === child.childrenUuid);
|
||||||
if (childGroup) {
|
if (childGroup) {
|
||||||
collectChildren(childGroup.childrens);
|
collectChildren(childGroup.children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
collectChildren(group.childrens);
|
collectChildren(group.children);
|
||||||
return allChildren;
|
return allChildren;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -429,11 +430,21 @@ export const createAssetGroupStore = () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getGroupsContainingAsset: (assetUuid) => {
|
getGroupsContainingAsset: (assetUuid) => {
|
||||||
return get().assetGroups.filter((group) => group.childrens.some((child) => child.type === "Asset" && child.childrenUuid === assetUuid));
|
return get().assetGroups.filter((group) => group.children.some((child) => child.type === "Asset" && child.childrenUuid === assetUuid));
|
||||||
},
|
},
|
||||||
|
|
||||||
getGroupsContainingGroup: (childGroupUuid) => {
|
getGroupsContainingGroup: (childGroupUuid) => {
|
||||||
return get().assetGroups.filter((group) => group.childrens.some((child) => child.type === "Group" && child.childrenUuid === childGroupUuid));
|
return get().assetGroups.filter((group) => group.children.some((child) => child.type === "Group" && child.childrenUuid === childGroupUuid));
|
||||||
|
},
|
||||||
|
|
||||||
|
getParentGroup: (item) => {
|
||||||
|
if (get().isGroup(item)) {
|
||||||
|
const parents = get().getGroupsContainingGroup(item.groupUuid);
|
||||||
|
return parents.length > 0 ? parents[0].groupUuid : null;
|
||||||
|
} else {
|
||||||
|
const parents = get().getGroupsContainingAsset(item.modelUuid);
|
||||||
|
return parents.length > 0 ? parents[0].groupUuid : null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hasGroup: (groupUuid) => {
|
hasGroup: (groupUuid) => {
|
||||||
@@ -446,7 +457,7 @@ export const createAssetGroupStore = () => {
|
|||||||
|
|
||||||
isEmptyGroup: (groupUuid) => {
|
isEmptyGroup: (groupUuid) => {
|
||||||
const group = get().assetGroups.find((g) => g.groupUuid === groupUuid);
|
const group = get().assetGroups.find((g) => g.groupUuid === groupUuid);
|
||||||
return !group || group.childrens.length === 0;
|
return !group || group.children.length === 0;
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
@@ -107,8 +108,7 @@
|
|||||||
|
|
||||||
&.root-drop-target {
|
&.root-drop-target {
|
||||||
background: var(--background-color-selected);
|
background: var(--background-color-selected);
|
||||||
box-shadow: inset 0 0 0 2px var(--border-color-accent),
|
box-shadow: inset 0 0 0 2px var(--border-color-accent), inset 0 0 20px var(--highlight-accent-color);
|
||||||
inset 0 0 20px var(--highlight-accent-color);
|
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-medium;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@@ -134,29 +134,42 @@
|
|||||||
.tree-node {
|
.tree-node {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
|
||||||
&.drop-target-highlight {
|
&.drop-target-highlight {
|
||||||
background: var(--background-color-selected);
|
background: var(--background-color-selected);
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-extra-large;
|
||||||
box-shadow: inset 0 0 0 2px var(--border-color-accent),
|
outline: 2px solid var(--border-color-accent);
|
||||||
0 0 20px var(--highlight-accent-color), 0 4px 12px var(--background-color-selected);
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.group-selected {
|
||||||
|
background: var(--background-color-selected);
|
||||||
|
border-radius: $border-radius-extra-large;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-node-content {
|
.tree-node-content {
|
||||||
@include flex-center;
|
@include flex-center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 12px;
|
padding: 4px 12px;
|
||||||
border-radius: $border-radius-medium;
|
border-radius: $border-radius-extra-large;
|
||||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
outline: 1px solid transparent;
|
||||||
|
outline-offset: -1px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--background-color-accent);
|
// background: var(--background-color-secondary);
|
||||||
|
outline: 1px solid var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: var(--background-color-selected);
|
span {
|
||||||
|
color: var(--text-button-color);
|
||||||
|
}
|
||||||
|
background: var(--background-color-accent);
|
||||||
|
outline: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dragging {
|
&.dragging {
|
||||||
@@ -264,11 +277,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(to bottom, var(--accent-color), var(--background-color-selected));
|
||||||
to bottom,
|
z-index: 100;
|
||||||
var(--accent-color),
|
|
||||||
var(--background-color-selected)
|
|
||||||
);
|
|
||||||
box-shadow: 0 0 4px var(--highlight-accent-color);
|
box-shadow: 0 0 4px var(--highlight-accent-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
app/src/types/builderTypes.d.ts
vendored
2
app/src/types/builderTypes.d.ts
vendored
@@ -56,7 +56,7 @@ interface AssetGroup {
|
|||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
childrens: {
|
children: {
|
||||||
type: "Asset" | "Group";
|
type: "Asset" | "Group";
|
||||||
childrenUuid: string;
|
childrenUuid: string;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
66
app/src/utils/useOuterClick.ts
Normal file
66
app/src/utils/useOuterClick.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function useOuterClick(
|
||||||
|
callback: () => void,
|
||||||
|
contextClassNames: string[],
|
||||||
|
enabled: boolean = true,
|
||||||
|
ignoreIfMoved: boolean = true,
|
||||||
|
clickType: "left" | "right" = "left",
|
||||||
|
dependencies: any[] = []
|
||||||
|
) {
|
||||||
|
const callbackRef = useRef(callback);
|
||||||
|
const classNamesRef = useRef(contextClassNames);
|
||||||
|
const mouseDownTargetRef = useRef<EventTarget | null>(null);
|
||||||
|
const movedRef = useRef(false);
|
||||||
|
|
||||||
|
// Keep refs updated
|
||||||
|
useEffect(() => {
|
||||||
|
callbackRef.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
classNamesRef.current = contextClassNames;
|
||||||
|
}, [contextClassNames]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
const handleMouseDown = (event: MouseEvent) => {
|
||||||
|
mouseDownTargetRef.current = event.target;
|
||||||
|
movedRef.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = () => {
|
||||||
|
movedRef.current = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = (event: MouseEvent) => {
|
||||||
|
const downTarget = mouseDownTargetRef.current as HTMLElement | null;
|
||||||
|
const upTarget = event.target as HTMLElement;
|
||||||
|
if (!downTarget || !upTarget) return;
|
||||||
|
|
||||||
|
// Check click type
|
||||||
|
const isCorrectClick = (clickType === "left" && event.button === 0) || (clickType === "right" && event.button === 2);
|
||||||
|
|
||||||
|
if (!isCorrectClick) return;
|
||||||
|
if (ignoreIfMoved && movedRef.current) return;
|
||||||
|
|
||||||
|
// Check if click is inside any ignored context
|
||||||
|
const isInside = classNamesRef.current.some((className) => downTarget.closest(`.${className}`) || upTarget.closest(`.${className}`));
|
||||||
|
|
||||||
|
if (!isInside) {
|
||||||
|
callbackRef.current();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleMouseDown);
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [enabled, ignoreIfMoved, clickType, ...dependencies]);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user