handleModelSort(key),
- }}
+ menu={modelSortMenu}
trigger={["click"]}
placement="bottomRight"
>
diff --git a/frontend/src/ide/ide-layout.css b/frontend/src/ide/ide-layout.css
index db14961..b3fcfec 100644
--- a/frontend/src/ide/ide-layout.css
+++ b/frontend/src/ide/ide-layout.css
@@ -80,8 +80,8 @@
}
/* Indent child/reference models to show hierarchy */
-.explorerTree .explorer-child-model {
- padding-left: 12px !important;
+.explorerTree .ant-tree-treenode.explorer-child-model {
+ padding-left: 12px;
}
.contextMenu {
From 1a8ef53061a54133a30d6d46652e8d1e77411192 Mon Sep 17 00:00:00 2001
From: wicky <130177258+wicky-zipstack@users.noreply.github.com>
Date: Mon, 13 Apr 2026 17:34:30 +0530
Subject: [PATCH 4/7] =?UTF-8?q?fix:=20address=20PR=20#56=20review=20?=
=?UTF-8?q?=E2=80=94=20extract=20shared=20lineage=20utils,=20CSS=20variabl?=
=?UTF-8?q?e,=20exec=5Forder?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Extract getRelatedNodeIds and applyScopedStyles into shared lineage-utils.js
- Remove duplicate functions from lineage-tab.jsx and no-code-model.jsx
- Move hardcoded #1677ff to --lineage-selected-border CSS variable (light: #1677ff, dark: #69b1ff)
- Add comment clarifying exec_order fallback uses backend's topological order
---
.../ide/editor/lineage-tab/lineage-tab.jsx | 75 +-------------
frontend/src/ide/editor/lineage-utils.js | 98 +++++++++++++++++++
.../editor/no-code-model/no-code-model.jsx | 74 +-------------
.../src/ide/explorer/explorer-component.jsx | 1 +
frontend/src/variables.css | 6 ++
5 files changed, 107 insertions(+), 147 deletions(-)
create mode 100644 frontend/src/ide/editor/lineage-utils.js
diff --git a/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx b/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx
index d10ec15..0905aae 100644
--- a/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx
+++ b/frontend/src/ide/editor/lineage-tab/lineage-tab.jsx
@@ -40,6 +40,7 @@ import { THEME } from "../../../common/constants.js";
import { SpinnerLoader } from "../../../widgets/spinner_loader/index.js";
import { useNotificationService } from "../../../service/notification-service.js";
import { Tech } from "../../../base/icons/index.js";
+import { applyScopedStyles } from "../lineage-utils.js";
import "reactflow/dist/style.css";
import "./lineage-tab.css";
@@ -289,80 +290,6 @@ const transformLineageData = (data) => {
return data;
};
-// Find all ancestor and descendant node IDs for a given model
-const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => {
- const nodeByLabel = {};
- allNodes.forEach((n) => {
- nodeByLabel[n.data.originalLabel || n.data.label] = n.id;
- });
- const selectedId = nodeByLabel[selectedLabel];
- if (!selectedId) return null;
-
- const related = new Set([selectedId]);
- const findAncestors = (id) => {
- allEdges.forEach((e) => {
- if (e.target === id && !related.has(e.source)) {
- related.add(e.source);
- findAncestors(e.source);
- }
- });
- };
- const findDescendants = (id) => {
- allEdges.forEach((e) => {
- if (e.source === id && !related.has(e.target)) {
- related.add(e.target);
- findDescendants(e.target);
- }
- });
- };
- findAncestors(selectedId);
- findDescendants(selectedId);
- return related;
-};
-
-const applyScopedStyles = (layoutedNodes, layoutedEdges, selectedLabel) => {
- const rawEdges = layoutedEdges.map((e) => ({
- source: e.source,
- target: e.target,
- }));
- const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes);
- if (!related) return { nodes: layoutedNodes, edges: layoutedEdges };
-
- const styledNodes = layoutedNodes.map((node) => {
- const nodeLabel = node.data.originalLabel || node.data.label;
- const isSelected = nodeLabel === selectedLabel;
- const isRelated = related.has(node.id);
- return {
- ...node,
- style: {
- ...node.style,
- opacity: isRelated ? 1 : 0.25,
- border: isSelected
- ? "2px dashed #1677ff"
- : node.style?.border || "1px solid var(--black)",
- },
- };
- });
-
- const relatedEdgeSet = new Set();
- layoutedEdges.forEach((e) => {
- if (related.has(e.source) && related.has(e.target)) {
- relatedEdgeSet.add(e.id);
- }
- });
-
- const styledEdges = layoutedEdges.map((edge) => ({
- ...edge,
- style: {
- ...edge.style,
- opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15,
- stroke: relatedEdgeSet.has(edge.id) ? "#1677ff" : undefined,
- },
- }));
-
- return { nodes: styledNodes, edges: styledEdges };
-};
-
function LineageTab({ nodeData, selectedModelName }) {
const axios = useAxiosPrivate();
const { selectedOrgId } = orgStore();
diff --git a/frontend/src/ide/editor/lineage-utils.js b/frontend/src/ide/editor/lineage-utils.js
new file mode 100644
index 0000000..37c159b
--- /dev/null
+++ b/frontend/src/ide/editor/lineage-utils.js
@@ -0,0 +1,98 @@
+/**
+ * Shared utility functions for lineage scoping.
+ * Used by both lineage-tab.jsx (standalone) and no-code-model.jsx (bottom section).
+ */
+
+/**
+ * Find all ancestor and descendant node IDs for a given model.
+ * @param {Array} allEdges - Array of { source, target } edge objects
+ * @param {string} selectedLabel - The label of the selected model
+ * @param {Array} allNodes - Array of node objects with data.originalLabel or data.label
+ * @return {Set|null} Set of related node IDs, or null if selected model not found
+ */
+export const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => {
+ const nodeByLabel = {};
+ allNodes.forEach((n) => {
+ nodeByLabel[n.data.originalLabel || n.data.label] = n.id;
+ });
+ const selectedId = nodeByLabel[selectedLabel];
+ if (!selectedId) return null;
+
+ const related = new Set([selectedId]);
+ const findAncestors = (id) => {
+ allEdges.forEach((e) => {
+ if (e.target === id && !related.has(e.source)) {
+ related.add(e.source);
+ findAncestors(e.source);
+ }
+ });
+ };
+ const findDescendants = (id) => {
+ allEdges.forEach((e) => {
+ if (e.source === id && !related.has(e.target)) {
+ related.add(e.target);
+ findDescendants(e.target);
+ }
+ });
+ };
+ findAncestors(selectedId);
+ findDescendants(selectedId);
+ return related;
+};
+
+/**
+ * Apply scoped styles to nodes and edges based on the selected model's lineage chain.
+ * Related nodes stay full opacity, unrelated nodes are faded.
+ * @param {Array} layoutedNodes - Array of positioned node objects
+ * @param {Array} layoutedEdges - Array of edge objects
+ * @param {string} selectedLabel - The label of the selected model
+ * @return {Object} { nodes, edges } with scoped styles applied
+ */
+export const applyScopedStyles = (
+ layoutedNodes,
+ layoutedEdges,
+ selectedLabel
+) => {
+ const rawEdges = layoutedEdges.map((e) => ({
+ source: e.source,
+ target: e.target,
+ }));
+ const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes);
+ if (!related) return { nodes: layoutedNodes, edges: layoutedEdges };
+
+ const styledNodes = layoutedNodes.map((node) => {
+ const nodeLabel = node.data.originalLabel || node.data.label;
+ const isSelected = nodeLabel === selectedLabel;
+ const isRelated = related.has(node.id);
+ return {
+ ...node,
+ style: {
+ ...node.style,
+ opacity: isRelated ? 1 : 0.25,
+ border: isSelected
+ ? "2px dashed var(--lineage-selected-border)"
+ : node.style?.border || "1px solid var(--black)",
+ },
+ };
+ });
+
+ const relatedEdgeSet = new Set();
+ layoutedEdges.forEach((e) => {
+ if (related.has(e.source) && related.has(e.target)) {
+ relatedEdgeSet.add(e.id);
+ }
+ });
+
+ const styledEdges = layoutedEdges.map((edge) => ({
+ ...edge,
+ style: {
+ ...edge.style,
+ opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15,
+ stroke: relatedEdgeSet.has(edge.id)
+ ? "var(--lineage-selected-border)"
+ : undefined,
+ },
+ }));
+
+ return { nodes: styledNodes, edges: styledEdges };
+};
diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx
index bb83974..5754c02 100644
--- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx
+++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx
@@ -69,6 +69,7 @@ import dagre from "dagre";
import { useAxiosPrivate } from "../../../service/axios-service.js";
import { NoCodeToolbar } from "../no-code-toolbar/no-code-toolbar.jsx";
import { NoCodeTopbar } from "../no-code-topbar/no-code-topbar.jsx";
+import { applyScopedStyles } from "../lineage-utils.js";
import { ConfigureSourceDestination } from "../no-code-configuration/configure-source-destination.jsx";
import { ConfigureJoins } from "../no-code-configuration/configure-joins.jsx";
import { useProjectStore } from "../../../store/project-store.js";
@@ -2171,79 +2172,6 @@ function NoCodeModel({ nodeData }) {
};
// Find all ancestor and descendant node IDs for a given model
- const getRelatedNodeIds = (allEdges, selectedLabel, allNodes) => {
- const nodeByLabel = {};
- allNodes.forEach((n) => {
- nodeByLabel[n.data.originalLabel || n.data.label] = n.id;
- });
- const selectedId = nodeByLabel[selectedLabel];
- if (!selectedId) return null;
-
- const related = new Set([selectedId]);
- const findAncestors = (id) => {
- allEdges.forEach((e) => {
- if (e.target === id && !related.has(e.source)) {
- related.add(e.source);
- findAncestors(e.source);
- }
- });
- };
- const findDescendants = (id) => {
- allEdges.forEach((e) => {
- if (e.source === id && !related.has(e.target)) {
- related.add(e.target);
- findDescendants(e.target);
- }
- });
- };
- findAncestors(selectedId);
- findDescendants(selectedId);
- return related;
- };
-
- const applyScopedStyles = (layoutedNodes, layoutedEdges, selectedLabel) => {
- const rawEdges = layoutedEdges.map((e) => ({
- source: e.source,
- target: e.target,
- }));
- const related = getRelatedNodeIds(rawEdges, selectedLabel, layoutedNodes);
- if (!related) return { nodes: layoutedNodes, edges: layoutedEdges };
-
- const styledNodes = layoutedNodes.map((node) => {
- const nodeLabel = node.data.originalLabel || node.data.label;
- const isSelected = nodeLabel === selectedLabel;
- const isRelated = related.has(node.id);
- return {
- ...node,
- style: {
- ...node.style,
- opacity: isRelated ? 1 : 0.25,
- border: isSelected
- ? "2px dashed #1677ff"
- : node.style?.border || "1px solid var(--black)",
- },
- };
- });
-
- const relatedEdgeSet = new Set();
- layoutedEdges.forEach((e) => {
- if (related.has(e.source) && related.has(e.target)) {
- relatedEdgeSet.add(e.id);
- }
- });
-
- const styledEdges = layoutedEdges.map((edge) => ({
- ...edge,
- style: {
- ...edge.style,
- opacity: relatedEdgeSet.has(edge.id) ? 1 : 0.15,
- stroke: relatedEdgeSet.has(edge.id) ? "#1677ff" : undefined,
- },
- }));
-
- return { nodes: styledNodes, edges: styledEdges };
- };
-
const getLineageData = (callSample = false) => {
if (!projectId) return;
setLineageData();
diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx
index 38c0d0b..13e2204 100644
--- a/frontend/src/ide/explorer/explorer-component.jsx
+++ b/frontend/src/ide/explorer/explorer-component.jsx
@@ -244,6 +244,7 @@ const IdeExplorer = ({
});
return result;
}
+ // "exec_order" and default: keep original backend order (topological/execution)
return [...models];
};
diff --git a/frontend/src/variables.css b/frontend/src/variables.css
index 036d9c4..f5db37f 100644
--- a/frontend/src/variables.css
+++ b/frontend/src/variables.css
@@ -81,6 +81,9 @@
--node-color-blue: #b0e3f9;
--node-color-yellow: #ffdd8a;
--node-color-pink: #ffc8d2;
+
+ /* Lineage scoping */
+ --lineage-selected-border: #1677ff;
}
.dark {
@@ -156,4 +159,7 @@
--node-color-blue: #1a5a6e;
--node-color-yellow: #6e5a1a;
--node-color-pink: #6e3a4a;
+
+ /* Lineage scoping */
+ --lineage-selected-border: #69b1ff;
}
From db58bbf8301ac44cecdbd81ce634264a2875be21 Mon Sep 17 00:00:00 2001
From: wicky <130177258+wicky-zipstack@users.noreply.github.com>
Date: Mon, 13 Apr 2026 17:49:00 +0530
Subject: [PATCH 5/7] =?UTF-8?q?fix:=20resolve=20temporal=20dead=20zone=20?=
=?UTF-8?q?=E2=80=94=20move=20handleModelSort=20after=20setTreeData?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- handleModelSort and modelSortMenu were defined before setTreeData, causing
'Cannot access setTreeData before initialization' ReferenceError
- Moved both after rebuildTree/setTreeData definition to fix initialization order
---
.../src/ide/explorer/explorer-component.jsx | 60 +++++++++----------
1 file changed, 30 insertions(+), 30 deletions(-)
diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx
index 13e2204..b825088 100644
--- a/frontend/src/ide/explorer/explorer-component.jsx
+++ b/frontend/src/ide/explorer/explorer-component.jsx
@@ -259,36 +259,8 @@ const IdeExplorer = ({
});
};
- const handleModelSort = useCallback(
- (sortBy) => {
- setModelSortBy(sortBy);
- if (rawTreeDataRef.current.length > 0) {
- const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current));
- freshData.forEach((node) => {
- if (node.title === "models" && node.children) {
- node.children.forEach((child) => {
- if (child.title === "no_code" && child.children) {
- child.children = sortModels(child.children, sortBy);
- applyModelDecorations(child.children);
- }
- });
- }
- });
- transformTree(freshData);
- setTreeData(freshData, false);
- }
- },
- [setTreeData]
- );
-
- const modelSortMenu = useMemo(
- () => ({
- items: MODEL_SORT_ITEMS,
- selectedKeys: [modelSortBy],
- onClick: ({ key }) => handleModelSort(key),
- }),
- [modelSortBy, handleModelSort]
- );
+ // handleModelSort and modelSortMenu are defined after setTreeData (line ~816)
+ // to avoid temporal dead zone — see sortMenuRef below for the Dropdown binding
// Function to map string icons from API to actual icon components
// depth: 0 = root (Database), 1 = schema, 2 = table, 3 = column
@@ -1099,6 +1071,34 @@ const IdeExplorer = ({
}
};
+ const handleModelSort = (sortBy) => {
+ setModelSortBy(sortBy);
+ if (rawTreeDataRef.current.length > 0) {
+ const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current));
+ freshData.forEach((node) => {
+ if (node.title === "models" && node.children) {
+ node.children.forEach((child) => {
+ if (child.title === "no_code" && child.children) {
+ child.children = sortModels(child.children, sortBy);
+ applyModelDecorations(child.children);
+ }
+ });
+ }
+ });
+ transformTree(freshData);
+ setTreeData(freshData, false);
+ }
+ };
+
+ const modelSortMenu = useMemo(
+ () => ({
+ items: MODEL_SORT_ITEMS,
+ selectedKeys: [modelSortBy],
+ onClick: ({ key }) => handleModelSort(key),
+ }),
+ [modelSortBy]
+ );
+
useEffect(() => {
if (schemaMenu) {
getExplorer(projectId);
From 0d13c473c00beeab78ad118880c090ded3b1e7c6 Mon Sep 17 00:00:00 2001
From: wicky <130177258+wicky-zipstack@users.noreply.github.com>
Date: Mon, 13 Apr 2026 18:06:52 +0530
Subject: [PATCH 6/7] fix: wrap handleModelSort in useCallback and add to
modelSortMenu deps
---
frontend/src/ide/explorer/explorer-component.jsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx
index b825088..20a3aec 100644
--- a/frontend/src/ide/explorer/explorer-component.jsx
+++ b/frontend/src/ide/explorer/explorer-component.jsx
@@ -1071,7 +1071,7 @@ const IdeExplorer = ({
}
};
- const handleModelSort = (sortBy) => {
+ const handleModelSort = useCallback((sortBy) => {
setModelSortBy(sortBy);
if (rawTreeDataRef.current.length > 0) {
const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current));
@@ -1088,7 +1088,7 @@ const IdeExplorer = ({
transformTree(freshData);
setTreeData(freshData, false);
}
- };
+ }, []);
const modelSortMenu = useMemo(
() => ({
@@ -1096,7 +1096,7 @@ const IdeExplorer = ({
selectedKeys: [modelSortBy],
onClick: ({ key }) => handleModelSort(key),
}),
- [modelSortBy]
+ [modelSortBy, handleModelSort]
);
useEffect(() => {
From 1a91142c8b91921746cdf709de3503dd10a2bd0b Mon Sep 17 00:00:00 2001
From: wicky <130177258+wicky-zipstack@users.noreply.github.com>
Date: Thu, 16 Apr 2026 09:47:17 +0530
Subject: [PATCH 7/7] fix: explorer sort dropdown UX, race condition, and No
Credits chip styling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Explorer sort dropdown:
- Use modelSortByRef to avoid stale closure in treeNoCodeModeTitleIcon
- Apply sort and decorations BEFORE transformTree so _isChild flag is set
when className is assigned (parent-child indent now displays correctly)
- Conditional indent: only show in Dependency Chain mode, not in Execution
Order/A→Z/Z→A modes
- Show checkmark on currently selected sort option in the dropdown menu
- useEffect rebuilds tree on modelSortBy change so dropdown reflects current state
Race condition fix:
- Move setRefreshModels(true) from handleSourceDestinationChange into
runTransformation .then() callback. Calling it before the run completed
triggered concurrent getExplorer/fetch_all_models that conflicted with
in-progress sync_file_models, causing "No module named" import errors.
No Credits chip styling:
- Match border and background with the standard info chip (raw, etc.)
- Only the text and icon stay red to indicate error state
- Now consistent across light and dark themes
---
frontend/src/ide/chat-ai/ChatAI.css | 6 +-
.../editor/no-code-model/no-code-model.jsx | 4 +-
.../src/ide/explorer/explorer-component.jsx | 88 ++++++++++++++++---
3 files changed, 82 insertions(+), 16 deletions(-)
diff --git a/frontend/src/ide/chat-ai/ChatAI.css b/frontend/src/ide/chat-ai/ChatAI.css
index 4988a75..8ac795b 100644
--- a/frontend/src/ide/chat-ai/ChatAI.css
+++ b/frontend/src/ide/chat-ai/ChatAI.css
@@ -441,12 +441,12 @@
cursor: pointer;
}
.chat-ai-info-chip-error {
- background-color: var(--ant-color-error-bg);
- border-color: var(--ant-color-error-border);
+ background-color: var(--content-bg);
+ border-color: var(--border-color-3);
}
.chat-ai-info-chip-error .chat-ai-info-chip-icon,
.chat-ai-info-chip-error .chat-ai-info-chip-text {
- color: var(--ant-color-error);
+ color: var(--error-color);
}
.chat-ai-manage-credits-link:hover {
color: var(--icons-color);
diff --git a/frontend/src/ide/editor/no-code-model/no-code-model.jsx b/frontend/src/ide/editor/no-code-model/no-code-model.jsx
index 5754c02..b29fec4 100644
--- a/frontend/src/ide/editor/no-code-model/no-code-model.jsx
+++ b/frontend/src/ide/editor/no-code-model/no-code-model.jsx
@@ -751,7 +751,6 @@ function NoCodeModel({ nodeData }) {
setSeqEdges(layoutedEdges);
runTransformation(res?.data?.model_data);
setConfigApply(true);
- setRefreshModels(true);
handleModalClose("ok");
})
.catch((error) => {
@@ -1614,6 +1613,9 @@ function NoCodeModel({ nodeData }) {
axios(requestOptions)
.then(() => {
getSampleData(undefined, undefined, spec);
+ // Trigger explorer refresh after run completes so updated references
+ // (from set-model save) reflect in the dependency chain sort
+ setRefreshModels(true);
})
.catch((error) => {
const notifKey = notify({
diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx
index 20a3aec..097c60d 100644
--- a/frontend/src/ide/explorer/explorer-component.jsx
+++ b/frontend/src/ide/explorer/explorer-component.jsx
@@ -65,7 +65,7 @@ import { useRefreshModelsStore } from "../../store/refresh-models-store.js";
import { LinearScale } from "../../base/icons";
// Static sort options for model explorer
-const MODEL_SORT_ITEMS = [
+const MODEL_SORT_OPTIONS = [
{ label: "Dependency Chain", key: "dep_chain" },
{ label: "Execution Order", key: "exec_order" },
{ label: "A \u2192 Z", key: "alpha_asc" },
@@ -143,6 +143,7 @@ const IdeExplorer = ({
const [uploading, setUploading] = useState(false);
const [fileList, setFileList] = useState([]);
const [modelSortBy, setModelSortBy] = useState("dep_chain");
+ const modelSortByRef = useRef("dep_chain");
const MAX_FILE_SIZE_MB = 50;
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
const { refreshModels, setRefreshModels } = useRefreshModelsStore();
@@ -248,13 +249,22 @@ const IdeExplorer = ({
return [...models];
};
- const applyModelDecorations = (models) => {
+ const applyModelDecorations = (models, sortBy) => {
+ // Only apply parent-child indent in Dependency Chain mode
+ if (sortBy !== "dep_chain") {
+ models.forEach((m) => {
+ delete m._isChild;
+ });
+ return;
+ }
// Build set of model names to distinguish from external table references
const modelNames = new Set(models.map((m) => m.title));
models.forEach((m) => {
const refs = (m.references || []).filter((r) => modelNames.has(r));
if (refs.length > 0) {
m._isChild = true;
+ } else {
+ delete m._isChild;
}
});
};
@@ -427,9 +437,8 @@ const IdeExplorer = ({
data.map((item) => {
if (item.title === "no_code") {
item.children = item.children || [];
- // Sort and decorate models
- item.children = sortModels(item.children, modelSortBy);
- applyModelDecorations(item.children);
+ // Note: sort and decorations are applied in getExplorer/rebuildTree/handleModelSort
+ // BEFORE transformTree, so _isChild flag is set when className is assigned
// Clean up stale selected model keys
const currentModelKeys = item.children.map((c) => c.key);
const filtered = selectedModelKeysRef.current.filter((k) =>
@@ -1066,21 +1075,40 @@ const IdeExplorer = ({
const rebuildTree = () => {
if (rawTreeDataRef.current.length > 0) {
const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current));
+ // Apply sort and decorations BEFORE transformTree so _isChild is set
+ freshData.forEach((node) => {
+ if (node.title === "models" && node.children) {
+ node.children.forEach((child) => {
+ if (child.title === "no_code" && child.children) {
+ child.children = sortModels(
+ child.children,
+ modelSortByRef.current
+ );
+ applyModelDecorations(child.children, modelSortByRef.current);
+ }
+ });
+ }
+ });
transformTree(freshData);
setTreeData(freshData, false);
}
};
const handleModelSort = useCallback((sortBy) => {
+ modelSortByRef.current = sortBy;
setModelSortBy(sortBy);
- if (rawTreeDataRef.current.length > 0) {
+ }, []);
+
+ // Rebuild tree when sort mode changes so the dropdown checkmark updates
+ useEffect(() => {
+ if (rawTreeDataRef.current && rawTreeDataRef.current.length > 0) {
const freshData = JSON.parse(JSON.stringify(rawTreeDataRef.current));
freshData.forEach((node) => {
if (node.title === "models" && node.children) {
node.children.forEach((child) => {
if (child.title === "no_code" && child.children) {
- child.children = sortModels(child.children, sortBy);
- applyModelDecorations(child.children);
+ child.children = sortModels(child.children, modelSortBy);
+ applyModelDecorations(child.children, modelSortBy);
}
});
}
@@ -1088,12 +1116,31 @@ const IdeExplorer = ({
transformTree(freshData);
setTreeData(freshData, false);
}
- }, []);
+ // eslint-disable-next-line
+ }, [modelSortBy]);
const modelSortMenu = useMemo(
() => ({
- items: MODEL_SORT_ITEMS,
- selectedKeys: [modelSortBy],
+ items: MODEL_SORT_OPTIONS.map((opt) => ({
+ key: opt.key,
+ label: (
+
+ {opt.label}
+ {modelSortBy === opt.key && (
+
+ )}
+
+ ),
+ })),
onClick: ({ key }) => handleModelSort(key),
}),
[modelSortBy, handleModelSort]
@@ -1113,6 +1160,21 @@ const IdeExplorer = ({
.then((res) => {
const treeData = res.data.children;
rawTreeDataRef.current = JSON.parse(JSON.stringify(treeData));
+ // Apply sort and decorations to no_code models BEFORE transformTree
+ // so that _isChild flag is set when className is assigned
+ treeData.forEach((node) => {
+ if (node.title === "models" && node.children) {
+ node.children.forEach((child) => {
+ if (child.title === "no_code" && child.children) {
+ child.children = sortModels(
+ child.children,
+ modelSortByRef.current
+ );
+ applyModelDecorations(child.children, modelSortByRef.current);
+ }
+ });
+ }
+ });
transformTree(treeData);
setTreeData(treeData);
@@ -2565,9 +2627,11 @@ function transformTree(tree) {
// change is_folder to isLeaf key and delete is_folder
delete Object.assign(node, { isLeaf: !node.is_folder }).is_folder;
- // Indent child/reference models
+ // Indent child/reference models (only in Dependency Chain sort)
if (node._isChild) {
node.className = "explorer-child-model";
+ } else if (node.className === "explorer-child-model") {
+ node.className = "";
}
if (node.children) {