Limit nodes opened by default in the JSON Tree component (#11002)

- Add a parameter to choose which nodes to open by default
- On the Admin Panel, open all nodes by default
- On the Workflow Run step output, open only the two first depths
- On the Workflow Run step input, open only the previous step first
depth
- Display `[empty string]` when a node is an empty string
- Now, display `null` instead of `[null]`

## Demo


https://github.com/user-attachments/assets/99b3078a-da3c-4330-b0ff-ddb2e360d933

Closes https://github.com/twentyhq/core-team-issues/issues/538
This commit is contained in:
Baptiste Devessier
2025-03-19 11:44:34 +01:00
committed by GitHub
parent 15a2cb5141
commit 1ecc5e2bf6
13 changed files with 141 additions and 11 deletions

View File

@ -6,13 +6,16 @@ import {
within,
} from '@storybook/test';
import { JsonTree } from '@ui/json-visualizer/components/JsonTree';
import { isTwoFirstDepths } from '@ui/json-visualizer/utils/isTwoFirstDepths';
const meta: Meta<typeof JsonTree> = {
title: 'UI/JsonVisualizer/JsonTree',
component: JsonTree,
args: {
shouldExpandNodeInitially: () => true,
emptyArrayLabel: 'Empty Array',
emptyObjectLabel: 'Empty Object',
emptyStringLabel: '[empty string]',
arrowButtonCollapsedLabel: 'Expand',
arrowButtonExpandedLabel: 'Collapse',
},
@ -273,6 +276,41 @@ export const ExpandingElementExpandsAllItsDescendants: Story = {
},
};
export const ExpandTwoFirstDepths: Story = {
args: {
value: {
person: {
name: 'John Doe',
address: {
street: '123 Main St',
city: 'New York',
country: {
name: 'USA',
code: 'US',
},
},
},
isActive: true,
},
shouldExpandNodeInitially: isTwoFirstDepths,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nameElement = await canvas.findByText('name');
expect(nameElement).toBeVisible();
const addressElement = await canvas.findByText('address');
expect(addressElement).toBeVisible();
const streetElement = canvas.queryByText('street');
expect(streetElement).not.toBeInTheDocument();
const countrCodeElement = canvas.queryByText('code');
expect(countrCodeElement).not.toBeInTheDocument();
},
};
export const ReallyDeepNestedObject: Story = {
args: {
value: {

View File

@ -5,6 +5,7 @@ import { JsonArrow } from '@ui/json-visualizer/components/internal/JsonArrow';
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { ANIMATION } from '@ui/theme';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
@ -49,9 +50,13 @@ export const JsonNestedNode = ({
depth: number;
keyPath: string;
}) => {
const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow();
const hideRoot = !isDefined(label);
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState(
shouldExpandNodeInitially({ keyPath, depth }),
);
const renderedChildren = (
<StyledJsonList

View File

@ -1,4 +1,10 @@
import { isBoolean, isNull, isNumber, isString } from '@sniptt/guards';
import {
isBoolean,
isNonEmptyString,
isNull,
isNumber,
isString,
} from '@sniptt/guards';
import {
IconCheckbox,
IconCircleOff,
@ -23,7 +29,7 @@ export const JsonNode = ({
depth: number;
keyPath: string;
}) => {
const { shouldHighlightNode } = useJsonTreeContextOrThrow();
const { shouldHighlightNode, emptyStringLabel } = useJsonTreeContextOrThrow();
const isHighlighted = shouldHighlightNode?.(keyPath) ?? false;
@ -31,7 +37,7 @@ export const JsonNode = ({
return (
<JsonValueNode
label={label}
valueAsString="[null]"
valueAsString="null"
Icon={IconCircleOff}
isHighlighted={isHighlighted}
/>
@ -42,7 +48,7 @@ export const JsonNode = ({
return (
<JsonValueNode
label={label}
valueAsString={value}
valueAsString={isNonEmptyString(value) ? value : emptyStringLabel}
Icon={IconTypography}
isHighlighted={isHighlighted}
/>

View File

@ -1,20 +1,27 @@
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
import { JsonTreeContextProvider } from '@ui/json-visualizer/components/JsonTreeContextProvider';
import { ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
import { JsonValue } from 'type-fest';
export const JsonTree = ({
value,
shouldHighlightNode,
shouldExpandNodeInitially,
emptyArrayLabel,
emptyObjectLabel,
emptyStringLabel,
arrowButtonCollapsedLabel,
arrowButtonExpandedLabel,
}: {
value: JsonValue;
shouldHighlightNode?: (keyPath: string) => boolean;
shouldExpandNodeInitially: (
params: ShouldExpandNodeInitiallyProps,
) => boolean;
emptyArrayLabel: string;
emptyObjectLabel: string;
emptyStringLabel: string;
arrowButtonCollapsedLabel: string;
arrowButtonExpandedLabel: string;
}) => {
@ -22,8 +29,10 @@ export const JsonTree = ({
<JsonTreeContextProvider
value={{
shouldHighlightNode,
shouldExpandNodeInitially,
emptyArrayLabel,
emptyObjectLabel,
emptyStringLabel,
arrowButtonCollapsedLabel,
arrowButtonExpandedLabel,
}}

View File

@ -1,7 +1,13 @@
import { createContext } from 'react';
export type ShouldExpandNodeInitiallyProps = { keyPath: string; depth: number };
export type JsonTreeContextType = {
shouldHighlightNode?: (keyPath: string) => boolean;
shouldExpandNodeInitially: (
params: ShouldExpandNodeInitiallyProps,
) => boolean;
emptyStringLabel: string;
emptyArrayLabel: string;
emptyObjectLabel: string;
arrowButtonCollapsedLabel: string;

View File

@ -8,3 +8,4 @@ export * from './components/JsonValueNode';
export * from './contexts/JsonTreeContext';
export * from './hooks/useJsonTreeContextOrThrow';
export * from './utils/isArray';
export * from './utils/isTwoFirstDepths';

View File

@ -0,0 +1,4 @@
import { ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
export const isTwoFirstDepths = ({ depth }: ShouldExpandNodeInitiallyProps) =>
depth <= 1;