mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Inspector revamp fixes
This commit is contained in:
parent
e6ee4e5bd8
commit
f1820e9620
9 changed files with 241 additions and 112 deletions
|
|
@ -43,7 +43,7 @@ const Row = ({ label, value, level = 1, absolutePath }) => {
|
|||
(isExpanded ? (
|
||||
<SolidIcon
|
||||
name="TriangleUpCenter"
|
||||
size={12}
|
||||
size={14}
|
||||
color="#1F99ED"
|
||||
style={{ marginRight: '4px' }}
|
||||
className="json-viewer-expand-icon"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.json-viewer-label-container {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { isEmpty } from 'lodash';
|
|||
|
||||
const JSONTreeViewerV2 = ({ data = {}, iconsList = [], darkMode, searchablePaths = new Set() }) => {
|
||||
const searchValue = useStore((state) => state.inspectorSearchValue, shallow);
|
||||
const getComponentIdFromName = useStore((state) => state.getComponentIdFromName, shallow);
|
||||
const getComponentDefinition = useStore((state) => state.getComponentDefinition, shallow);
|
||||
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
|
||||
const setSearchValue = useStore((state) => state.setInspectorSearchValue, shallow);
|
||||
const [selectedNodePath, setSelectedNodePath] = React.useState(null);
|
||||
|
|
@ -76,7 +78,27 @@ const JSONTreeViewerV2 = ({ data = {}, iconsList = [], darkMode, searchablePaths
|
|||
setSelectedNodePath(null);
|
||||
};
|
||||
|
||||
const selectedData = selectedNodePath ? getResolvedValue(`{{${selectedNodePath}}}`) : {};
|
||||
const selectedData = (() => {
|
||||
if (selectedNodePath?.startsWith('components.')) {
|
||||
// Split the selectedNode path using . and grab the second element if it exists
|
||||
const pathArray = selectedNodePath.split('.');
|
||||
const componentName = pathArray?.[1];
|
||||
const componentId = getComponentIdFromName(componentName);
|
||||
const component = getComponentDefinition(componentId);
|
||||
const parent = component?.component?.parent;
|
||||
if (parent) {
|
||||
const parentComponent = getComponentDefinition(parent);
|
||||
const parentType = parentComponent?.component?.component;
|
||||
if (parentType === 'Form') {
|
||||
return {
|
||||
id: componentId,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return selectedNodePath ? getResolvedValue(`{{${selectedNodePath}}}`) : {};
|
||||
})();
|
||||
|
||||
const expandedIds = [...Array.from(pathSet), ...selectedNodes];
|
||||
|
||||
const filteredIds = useMemo(() => {
|
||||
|
|
@ -86,7 +108,20 @@ const JSONTreeViewerV2 = ({ data = {}, iconsList = [], darkMode, searchablePaths
|
|||
const { path } = metadata || {};
|
||||
return expandedIdsSet.has(path);
|
||||
});
|
||||
return filtered.map((item) => item.id);
|
||||
|
||||
return filtered
|
||||
.map((item) => item.id)
|
||||
.filter((path) => {
|
||||
const pathArray = path.split('.');
|
||||
// One by one combine and check if the path is in expandedIds or not
|
||||
for (let i = pathArray.length - 1; i > 0; i--) {
|
||||
const parentPath = pathArray.slice(0, i).join('.');
|
||||
if (!expandedIdsSet.has(parentPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [flattendedData, expandedIds]);
|
||||
|
||||
return (
|
||||
|
|
@ -105,35 +140,36 @@ const JSONTreeViewerV2 = ({ data = {}, iconsList = [], darkMode, searchablePaths
|
|||
width={300}
|
||||
/>
|
||||
</div>
|
||||
<div className="json-tree-view">
|
||||
<TreeView
|
||||
data={flattendedData}
|
||||
className="basic"
|
||||
aria-label="basic example tree"
|
||||
defaultExpandedIds={selectedNodes}
|
||||
expandedIds={filteredIds}
|
||||
key={key}
|
||||
nodeRenderer={(props) => {
|
||||
const { element } = props;
|
||||
const { metadata } = element || {};
|
||||
const { path } = metadata || {};
|
||||
const data = {
|
||||
nodeName: element.name,
|
||||
selectedNodePath: path,
|
||||
};
|
||||
|
||||
<TreeView
|
||||
data={flattendedData}
|
||||
className="basic"
|
||||
aria-label="basic example tree"
|
||||
defaultExpandedIds={selectedNodes}
|
||||
expandedIds={filteredIds}
|
||||
key={key}
|
||||
nodeRenderer={(props) => {
|
||||
const { element } = props;
|
||||
const { metadata } = element || {};
|
||||
const { path } = metadata || {};
|
||||
const data = {
|
||||
nodeName: element.name,
|
||||
selectedNodePath: path,
|
||||
};
|
||||
|
||||
return (
|
||||
<Node
|
||||
{...props}
|
||||
darkMode={darkMode}
|
||||
setSelectedNodePath={setSelectedNodePath}
|
||||
searchValue={searchValue}
|
||||
iconsList={iconsList}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
return (
|
||||
<Node
|
||||
{...props}
|
||||
darkMode={darkMode}
|
||||
setSelectedNodePath={setSelectedNodePath}
|
||||
searchValue={searchValue}
|
||||
iconsList={iconsList}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<JSONViewer data={selectedData} darkMode={darkMode} path={selectedNodePath} backFn={backFn} />
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import JSONTreeViewerV2 from './JSONTreeViewerV2';
|
|||
import _ from 'lodash';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import useIconList from './useIconList';
|
||||
|
||||
import { formatInspectorComponentData, formatInspectorDataMisc, formatInspectorQueryData } from './utils';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button';
|
||||
import { formatInspectorDataMisc, formatInspectorQueryData } from './utils';
|
||||
|
||||
const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => {
|
||||
const exposedComponentsVariables = useStore((state) => state.getAllExposedValues().components, shallow);
|
||||
|
|
@ -18,6 +18,7 @@ const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => {
|
|||
const exposedPageVariables = useStore((state) => state.getAllExposedValues().page || {}, shallow);
|
||||
const exposedGlobalVariables = useStore((state) => state.getAllExposedValues().globals || {}, shallow);
|
||||
const componentIdNameMapping = useStore((state) => state.getComponentIdNameMapping(), shallow);
|
||||
const formatInspectorComponentData = useStore((state) => state.formatInspectorComponentData, shallow);
|
||||
const queryNameIdMapping = useStore((state) => state.getQueryNameIdMapping(), shallow);
|
||||
const searchablePaths = useRef(new Set(['queries', 'components', 'globals', 'variables', 'page', 'constants']));
|
||||
|
||||
|
|
@ -109,18 +110,14 @@ const LeftSidebarInspector = ({ darkMode, pinned, setPinned }) => {
|
|||
<HeaderSection darkMode={darkMode}>
|
||||
<HeaderSection.PanelHeader title="State inspector">
|
||||
<div className="d-flex justify-content-end">
|
||||
<ButtonSolid
|
||||
title={`${pinned ? 'Unpin' : 'Pin'}`}
|
||||
<ButtonComponent
|
||||
iconOnly
|
||||
leadingIcon={pinned ? 'unpin' : 'pin'}
|
||||
onClick={() => setPinned(!pinned)}
|
||||
darkMode={darkMode}
|
||||
styles={{ width: '28px', padding: 0 }}
|
||||
data-cy={`left-sidebar-inspector`}
|
||||
variant="ghostBlack"
|
||||
className="left-sidebar-header-btn"
|
||||
leftIcon={pinned ? 'unpin' : 'pin'}
|
||||
iconWidth="14"
|
||||
fill={`var(--slate12)`}
|
||||
></ButtonSolid>
|
||||
variant="ghost"
|
||||
fill="var(--icon-strong,#6A727C)"
|
||||
size="medium"
|
||||
/>
|
||||
</div>
|
||||
</HeaderSection.PanelHeader>
|
||||
</HeaderSection>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import OverflowTooltip from '@/_components/OverflowTooltip';
|
|||
import { HiddenOptions } from './HiddenOptions';
|
||||
import useCallbackActions from './useCallbackActions';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { Button as ButtonComponent } from '@/components/ui/Button/Button';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
const renderNodeIcons = (node, iconsList, darkMode) => {
|
||||
|
|
@ -56,21 +57,26 @@ export const Node = (props) => {
|
|||
const setSelectedNodes = useStore((state) => state.setSelectedNodes, shallow);
|
||||
const callbackActions = useCallbackActions() || [];
|
||||
const nodeIcon = renderNodeIcons(element.name, iconsList, darkMode);
|
||||
const getResolvedValue = useStore((state) => state.getResolvedValue, shallow);
|
||||
const metadata = element.metadata || {};
|
||||
const { type } = metadata;
|
||||
const { type, path } = metadata;
|
||||
const nodeSpecificActions = callbackActions.filter((action) => [type].includes(action.for));
|
||||
const onExpand = (node) => {
|
||||
const { element } = node || {};
|
||||
const { metadata } = element || {};
|
||||
const { path } = metadata || {};
|
||||
setSelectedNodes(path);
|
||||
};
|
||||
|
||||
const onSelect = (node) => {
|
||||
const { isBranch, element } = node || {};
|
||||
const { metadata } = element || {};
|
||||
const { path } = metadata || {};
|
||||
if (!isBranch) {
|
||||
const { path, type } = metadata || {};
|
||||
if (type) {
|
||||
setSelectedNodePath(path);
|
||||
} else {
|
||||
setSelectedNodePath(null);
|
||||
}
|
||||
|
||||
setSelectedNodes(path);
|
||||
};
|
||||
|
||||
const nodeSpecificFilteredActions =
|
||||
nodeSpecificActions?.[0]?.actions?.filter((action) => {
|
||||
return action.enableInspectorTreeView;
|
||||
|
|
@ -84,27 +90,34 @@ export const Node = (props) => {
|
|||
return (
|
||||
// <div {...getNodeProps({ onClick: handleExpand })}>
|
||||
<div
|
||||
onClick={() => onSelect(props)}
|
||||
style={{
|
||||
marginLeft: 22 * (level - 1),
|
||||
marginLeft: level > 1 ? 12 : 0,
|
||||
// paddingLeft: '16px',
|
||||
opacity: isDisabled ? 0.5 : 1,
|
||||
height: level === 1 ? '28px' : '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: level === 1 ? 'var(--text-placeholder, #6A727C)' : 'var(--text-default, #1B1F24)',
|
||||
cursor: isBranch || level === 1 ? 'pointer' : 'default',
|
||||
// borderLeft: level > 1 ? '1px solid var(--slate6, #D7DBDF)' : 'none',
|
||||
}}
|
||||
>
|
||||
{(isBranch || level === 1) && (
|
||||
{!['queries', 'globals', 'variables'].includes(type) && (
|
||||
<div className="node-expansion-icon">
|
||||
{isExpanded ? (
|
||||
<SolidIcon name="TriangleDownCenter" width="16" height="16" />
|
||||
) : (
|
||||
<SolidIcon name="TriangleUpCenter" width="16" height="16" />
|
||||
{(isBranch || level === 1 || path === 'page.variables') && (
|
||||
<ButtonComponent
|
||||
iconOnly
|
||||
leadingIcon={isExpanded ? 'TriangleDownCenter' : 'rightarrrow'}
|
||||
onClick={() => onExpand(props)}
|
||||
variant="ghost"
|
||||
fill="var(--icon-default,#ACB2B9)"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
onClick={() => onSelect(props)}
|
||||
className={cx('node-content', {
|
||||
'node-content-hoverable': level !== 1,
|
||||
'node-content-active': actionClicked,
|
||||
|
|
@ -112,7 +125,7 @@ export const Node = (props) => {
|
|||
>
|
||||
{nodeIcon && <div className="node-icon">{nodeIcon}</div>}
|
||||
<div className="node-label">
|
||||
<OverflowTooltip whiteSpace="normal" placement="top" style={{ height: '100%', width: '190px' }}>
|
||||
<OverflowTooltip whiteSpace="normal" placement="top" style={{ height: '100%', width: '80%' }}>
|
||||
<Highlighter
|
||||
highlightClassName="node-highlight"
|
||||
searchWords={[searchValue]}
|
||||
|
|
|
|||
|
|
@ -138,6 +138,8 @@ export const TreeViewHeader = (props) => {
|
|||
className="copy-menu-options-icon json-viewer-options-btn"
|
||||
style={{
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<SolidIcon data-cy={'menu-icon'} name="morevertical" width="18" fill={'#6A727C'} />
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export const formatInspectorDataMisc = (obj, type, searchablePaths = new Set())
|
|||
name,
|
||||
children: reduceData(value, currentPath, level + 1),
|
||||
metadata: {
|
||||
type: 'misc',
|
||||
type: type,
|
||||
path: currentPath,
|
||||
...((path === 'page.variables' ? level === 2 : level === 1) && {
|
||||
data: typeof value === 'object' ? JSON.stringify(value) : value,
|
||||
|
|
@ -33,49 +33,6 @@ export const formatInspectorDataMisc = (obj, type, searchablePaths = new Set())
|
|||
return reduceData(data, type);
|
||||
};
|
||||
|
||||
export const formatInspectorComponentData = (
|
||||
componentIdNameMapping,
|
||||
exposedComponentsVariables,
|
||||
searchablePaths = new Set()
|
||||
) => {
|
||||
const data = Object.entries(componentIdNameMapping)
|
||||
.map(([key, name]) => ({
|
||||
key,
|
||||
name: name || key,
|
||||
value: exposedComponentsVariables[key] ?? { id: key },
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
||||
|
||||
const reduceData = (obj, path = 'components', level = 1) => {
|
||||
let data = obj;
|
||||
if (!obj || typeof obj !== 'object' || level > 1) return [];
|
||||
else if (!Array.isArray(obj)) {
|
||||
data = Object.entries(obj);
|
||||
}
|
||||
return data
|
||||
.filter((item) => item.name)
|
||||
.reduce((acc, { key, name, value }) => {
|
||||
const currentPath = path + `.${name}`;
|
||||
searchablePaths.add(currentPath);
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
id: currentPath,
|
||||
name,
|
||||
children: reduceData(value, currentPath, level + 1),
|
||||
metadata: {
|
||||
type: 'components',
|
||||
path: currentPath,
|
||||
...(level === 1 && { data: typeof value === 'object' ? JSON.stringify(value) : value }),
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
|
||||
return reduceData(data);
|
||||
};
|
||||
|
||||
export const formatInspectorQueryData = (queryNameIdMapping, exposedQueries, searchablePaths = new Set()) => {
|
||||
const reverseMapping = Object.fromEntries(Object.entries(queryNameIdMapping).map(([name, id]) => [id, name]));
|
||||
const _sortedQueries = Object.entries(exposedQueries)
|
||||
|
|
|
|||
|
|
@ -31,4 +31,103 @@ export const createInspectorSlice = (set, get) => ({
|
|||
setInspectorSearchResults: (results) => {
|
||||
set({ inspectorSearchResults: results });
|
||||
},
|
||||
getAllComponentChildrenById: (id) => {
|
||||
const { getComponentDefinition, getResolvedComponent } = get();
|
||||
const component = getComponentDefinition(id);
|
||||
const componentType = component?.component?.component;
|
||||
switch (componentType) {
|
||||
case 'Container':
|
||||
case 'Form':
|
||||
case 'ModalV2':
|
||||
return [
|
||||
...get().getContainerChildrenMapping(id),
|
||||
...get().getContainerChildrenMapping(`${id}-header`),
|
||||
...get().getContainerChildrenMapping(`${id}-footer`),
|
||||
];
|
||||
case 'Tabs': {
|
||||
const tabs = getResolvedComponent(id)?.properties?.tabs;
|
||||
const children = Array.isArray(tabs) ? tabs : [];
|
||||
const res = children
|
||||
?.map((tab) => {
|
||||
const tabId = `${id}-${tab.id}`;
|
||||
return get().getContainerChildrenMapping(tabId);
|
||||
})
|
||||
.reduce((acc, curr) => {
|
||||
return [...acc, ...curr];
|
||||
}, []);
|
||||
return res;
|
||||
}
|
||||
default:
|
||||
return get().getContainerChildrenMapping(id);
|
||||
}
|
||||
},
|
||||
|
||||
formatInspectorComponentData: (
|
||||
componentIdNameMapping,
|
||||
exposedComponentsVariables,
|
||||
searchablePaths = new Set(),
|
||||
moduleId = 'canvas'
|
||||
) => {
|
||||
const { getComponentDefinition, getAllComponentChildrenById } = get();
|
||||
const data = Object.entries(componentIdNameMapping)
|
||||
.filter(([key]) => {
|
||||
const component = getComponentDefinition(key, moduleId);
|
||||
return !component?.component?.parent;
|
||||
})
|
||||
.map(([key, name]) => {
|
||||
const component = getComponentDefinition(key, moduleId);
|
||||
let parentComponentType = null;
|
||||
if (component?.component?.parent) {
|
||||
const parentComponent = getComponentDefinition(component.component.parent, moduleId);
|
||||
parentComponentType = parentComponent?.component?.component;
|
||||
}
|
||||
return {
|
||||
key,
|
||||
name: name || key,
|
||||
parentType: parentComponentType,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
||||
|
||||
const reduceData = (obj, path = 'components', level = 1) => {
|
||||
let data = obj;
|
||||
if (!obj || typeof obj !== 'object') return [];
|
||||
|
||||
return data
|
||||
.filter((item) => item.name)
|
||||
.reduce((acc, { key, name, parentType }) => {
|
||||
const currentPath = `components.${name}`;
|
||||
searchablePaths.add(currentPath);
|
||||
const children = getAllComponentChildrenById(key).map((childKey) => {
|
||||
const childComponent = getComponentDefinition(childKey);
|
||||
let parentComponentType = null;
|
||||
if (childComponent?.component?.parent) {
|
||||
const parentComponent = getComponentDefinition(childComponent.component.parent);
|
||||
parentComponentType = parentComponent?.component?.component;
|
||||
}
|
||||
return {
|
||||
key: childKey,
|
||||
name: childComponent?.component?.name,
|
||||
parentType: parentComponentType,
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
...acc,
|
||||
{
|
||||
id: currentPath,
|
||||
name,
|
||||
children: reduceData(children, currentPath, level + 1),
|
||||
metadata: {
|
||||
type: 'components',
|
||||
path: currentPath,
|
||||
parentType: parentType,
|
||||
},
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
|
||||
return reduceData(data);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18877,12 +18877,24 @@ section.ai-message-prompt-input-wrapper {
|
|||
}
|
||||
}
|
||||
|
||||
.json-tree-view {
|
||||
ul {
|
||||
margin-left:16px !important;
|
||||
border-left: 1px solid var(--slate6, #D7DBDF);
|
||||
}
|
||||
|
||||
ul[role="tree"] {
|
||||
border-left: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.basic.tree {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
padding: 0px;
|
||||
padding-right:20px;
|
||||
padding-top: 0px;
|
||||
|
||||
}
|
||||
|
||||
.basic .tree-node,
|
||||
|
|
@ -18919,6 +18931,7 @@ section.ai-message-prompt-input-wrapper {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
|
|
@ -18932,6 +18945,8 @@ section.ai-message-prompt-input-wrapper {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
font-size:12px;
|
||||
flex:1;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -19016,16 +19031,24 @@ section.ai-message-prompt-input-wrapper {
|
|||
|
||||
}
|
||||
|
||||
// .json-viewer-options-btn {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// margin-left: auto;
|
||||
|
||||
// &:hover {
|
||||
// cursor: pointer;
|
||||
// background-color:var(--interactive-overlays-fill-hover);
|
||||
// border-radius: 4px;
|
||||
// }
|
||||
// }
|
||||
|
||||
.json-viewer-options-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color:var(--interactive-overlays-fill-hover);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -19052,6 +19075,7 @@ section.ai-message-prompt-input-wrapper {
|
|||
width: 20px;
|
||||
border-radius: 4px;
|
||||
margin-right:4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-outline-hover);
|
||||
|
|
|
|||
Loading…
Reference in a new issue