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:
committed by
GitHub
parent
15a2cb5141
commit
1ecc5e2bf6
@ -85,11 +85,17 @@ export const CommandMenuWorkflowRunViewStep = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{activeTabId === 'input' ? (
|
{activeTabId === 'input' ? (
|
||||||
<WorkflowRunStepInputDetail stepId={workflowSelectedNode} />
|
<WorkflowRunStepInputDetail
|
||||||
|
key={workflowSelectedNode}
|
||||||
|
stepId={workflowSelectedNode}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{activeTabId === 'output' ? (
|
{activeTabId === 'output' ? (
|
||||||
<WorkflowRunStepOutputDetail stepId={workflowSelectedNode} />
|
<WorkflowRunStepOutputDetail
|
||||||
|
key={workflowSelectedNode}
|
||||||
|
stepId={workflowSelectedNode}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</WorkflowStepContextProvider>
|
</WorkflowStepContextProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -33,6 +33,8 @@ export const JsonDataIndicatorHealthStatus = () => {
|
|||||||
!indicatorHealth.status ||
|
!indicatorHealth.status ||
|
||||||
indicatorHealth.status === AdminPanelHealthServiceStatus.OUTAGE;
|
indicatorHealth.status === AdminPanelHealthServiceStatus.OUTAGE;
|
||||||
|
|
||||||
|
const isAnyNode = () => true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
{isDown && (
|
{isDown && (
|
||||||
@ -45,8 +47,10 @@ export const JsonDataIndicatorHealthStatus = () => {
|
|||||||
<StyledDetailsContainer>
|
<StyledDetailsContainer>
|
||||||
<JsonTree
|
<JsonTree
|
||||||
value={parsedDetails}
|
value={parsedDetails}
|
||||||
|
shouldExpandNodeInitially={isAnyNode}
|
||||||
emptyArrayLabel={t`Empty Array`}
|
emptyArrayLabel={t`Empty Array`}
|
||||||
emptyObjectLabel={t`Empty Object`}
|
emptyObjectLabel={t`Empty Object`}
|
||||||
|
emptyStringLabel={t`[empty string]`}
|
||||||
arrowButtonCollapsedLabel={t`Expand`}
|
arrowButtonCollapsedLabel={t`Expand`}
|
||||||
arrowButtonExpandedLabel={t`Collapse`}
|
arrowButtonExpandedLabel={t`Collapse`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -85,11 +85,17 @@ export const RightDrawerWorkflowRunViewStep = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{activeTabId === 'input' ? (
|
{activeTabId === 'input' ? (
|
||||||
<WorkflowRunStepInputDetail stepId={workflowSelectedNode} />
|
<WorkflowRunStepInputDetail
|
||||||
|
key={workflowSelectedNode}
|
||||||
|
stepId={workflowSelectedNode}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{activeTabId === 'output' ? (
|
{activeTabId === 'output' ? (
|
||||||
<WorkflowRunStepOutputDetail stepId={workflowSelectedNode} />
|
<WorkflowRunStepOutputDetail
|
||||||
|
key={workflowSelectedNode}
|
||||||
|
stepId={workflowSelectedNode}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</WorkflowStepContextProvider>
|
</WorkflowStepContextProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||||
|
import { getWorkflowPreviousStepId } from '@/workflow/workflow-steps/utils/getWorkflowPreviousStep';
|
||||||
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||||
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
|
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@ -9,6 +10,7 @@ import {
|
|||||||
IconBrackets,
|
IconBrackets,
|
||||||
JsonNestedNode,
|
JsonNestedNode,
|
||||||
JsonTreeContextProvider,
|
JsonTreeContextProvider,
|
||||||
|
ShouldExpandNodeInitiallyProps,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -38,6 +40,15 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previousStepId = getWorkflowPreviousStepId({
|
||||||
|
stepId,
|
||||||
|
steps: workflowRun.output.flow.steps,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (previousStepId === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const variablesUsedInStep = getWorkflowVariablesUsedInStep({
|
const variablesUsedInStep = getWorkflowVariablesUsedInStep({
|
||||||
step,
|
step,
|
||||||
});
|
});
|
||||||
@ -47,20 +58,27 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
|||||||
flow: workflowRun.output.flow,
|
flow: workflowRun.output.flow,
|
||||||
stepId,
|
stepId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stepContext.length === 0) {
|
if (stepContext.length === 0) {
|
||||||
throw new Error('The input tab must be rendered with a non-empty context.');
|
throw new Error('The input tab must be rendered with a non-empty context.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFirstNodeDepthOfPreviousStep = ({
|
||||||
|
keyPath,
|
||||||
|
depth,
|
||||||
|
}: ShouldExpandNodeInitiallyProps) =>
|
||||||
|
keyPath.startsWith(previousStepId) && depth < 2;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<JsonTreeContextProvider
|
<JsonTreeContextProvider
|
||||||
value={{
|
value={{
|
||||||
emptyArrayLabel: t`Empty Array`,
|
emptyArrayLabel: t`Empty Array`,
|
||||||
emptyObjectLabel: t`Empty Object`,
|
emptyObjectLabel: t`Empty Object`,
|
||||||
|
emptyStringLabel: t`[empty string]`,
|
||||||
arrowButtonCollapsedLabel: t`Expand`,
|
arrowButtonCollapsedLabel: t`Expand`,
|
||||||
arrowButtonExpandedLabel: t`Collapse`,
|
arrowButtonExpandedLabel: t`Collapse`,
|
||||||
shouldHighlightNode: (keyPath) => variablesUsedInStep.has(keyPath),
|
shouldHighlightNode: (keyPath) => variablesUsedInStep.has(keyPath),
|
||||||
|
shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<JsonNestedNode
|
<JsonNestedNode
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThro
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { JsonTree } from 'twenty-ui';
|
import { isTwoFirstDepths, JsonTree } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -28,8 +28,10 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
|||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<JsonTree
|
<JsonTree
|
||||||
value={stepOutput}
|
value={stepOutput}
|
||||||
|
shouldExpandNodeInitially={isTwoFirstDepths}
|
||||||
emptyArrayLabel={t`Empty Array`}
|
emptyArrayLabel={t`Empty Array`}
|
||||||
emptyObjectLabel={t`Empty Object`}
|
emptyObjectLabel={t`Empty Object`}
|
||||||
|
emptyStringLabel={t`[empty string]`}
|
||||||
arrowButtonCollapsedLabel={t`Expand`}
|
arrowButtonCollapsedLabel={t`Expand`}
|
||||||
arrowButtonExpandedLabel={t`Collapse`}
|
arrowButtonExpandedLabel={t`Collapse`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { WorkflowStep } from '@/workflow/types/Workflow';
|
||||||
|
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
|
||||||
|
|
||||||
|
export const getWorkflowPreviousStepId = ({
|
||||||
|
stepId,
|
||||||
|
steps,
|
||||||
|
}: {
|
||||||
|
stepId: string;
|
||||||
|
steps: Array<WorkflowStep>;
|
||||||
|
}) => {
|
||||||
|
if (stepId === TRIGGER_STEP_ID) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepId === steps[0].id) {
|
||||||
|
return TRIGGER_STEP_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepIndex = steps.findIndex((step) => step.id === stepId);
|
||||||
|
if (stepIndex === -1) {
|
||||||
|
throw new Error('Step not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps[stepIndex - 1].id;
|
||||||
|
};
|
||||||
@ -6,13 +6,16 @@ import {
|
|||||||
within,
|
within,
|
||||||
} from '@storybook/test';
|
} from '@storybook/test';
|
||||||
import { JsonTree } from '@ui/json-visualizer/components/JsonTree';
|
import { JsonTree } from '@ui/json-visualizer/components/JsonTree';
|
||||||
|
import { isTwoFirstDepths } from '@ui/json-visualizer/utils/isTwoFirstDepths';
|
||||||
|
|
||||||
const meta: Meta<typeof JsonTree> = {
|
const meta: Meta<typeof JsonTree> = {
|
||||||
title: 'UI/JsonVisualizer/JsonTree',
|
title: 'UI/JsonVisualizer/JsonTree',
|
||||||
component: JsonTree,
|
component: JsonTree,
|
||||||
args: {
|
args: {
|
||||||
|
shouldExpandNodeInitially: () => true,
|
||||||
emptyArrayLabel: 'Empty Array',
|
emptyArrayLabel: 'Empty Array',
|
||||||
emptyObjectLabel: 'Empty Object',
|
emptyObjectLabel: 'Empty Object',
|
||||||
|
emptyStringLabel: '[empty string]',
|
||||||
arrowButtonCollapsedLabel: 'Expand',
|
arrowButtonCollapsedLabel: 'Expand',
|
||||||
arrowButtonExpandedLabel: 'Collapse',
|
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 = {
|
export const ReallyDeepNestedObject: Story = {
|
||||||
args: {
|
args: {
|
||||||
value: {
|
value: {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { JsonArrow } from '@ui/json-visualizer/components/internal/JsonArrow';
|
|||||||
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
|
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
|
||||||
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
|
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
|
||||||
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
|
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
|
||||||
|
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||||
import { ANIMATION } from '@ui/theme';
|
import { ANIMATION } from '@ui/theme';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@ -49,9 +50,13 @@ export const JsonNestedNode = ({
|
|||||||
depth: number;
|
depth: number;
|
||||||
keyPath: string;
|
keyPath: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow();
|
||||||
|
|
||||||
const hideRoot = !isDefined(label);
|
const hideRoot = !isDefined(label);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(
|
||||||
|
shouldExpandNodeInitially({ keyPath, depth }),
|
||||||
|
);
|
||||||
|
|
||||||
const renderedChildren = (
|
const renderedChildren = (
|
||||||
<StyledJsonList
|
<StyledJsonList
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { isBoolean, isNull, isNumber, isString } from '@sniptt/guards';
|
import {
|
||||||
|
isBoolean,
|
||||||
|
isNonEmptyString,
|
||||||
|
isNull,
|
||||||
|
isNumber,
|
||||||
|
isString,
|
||||||
|
} from '@sniptt/guards';
|
||||||
import {
|
import {
|
||||||
IconCheckbox,
|
IconCheckbox,
|
||||||
IconCircleOff,
|
IconCircleOff,
|
||||||
@ -23,7 +29,7 @@ export const JsonNode = ({
|
|||||||
depth: number;
|
depth: number;
|
||||||
keyPath: string;
|
keyPath: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { shouldHighlightNode } = useJsonTreeContextOrThrow();
|
const { shouldHighlightNode, emptyStringLabel } = useJsonTreeContextOrThrow();
|
||||||
|
|
||||||
const isHighlighted = shouldHighlightNode?.(keyPath) ?? false;
|
const isHighlighted = shouldHighlightNode?.(keyPath) ?? false;
|
||||||
|
|
||||||
@ -31,7 +37,7 @@ export const JsonNode = ({
|
|||||||
return (
|
return (
|
||||||
<JsonValueNode
|
<JsonValueNode
|
||||||
label={label}
|
label={label}
|
||||||
valueAsString="[null]"
|
valueAsString="null"
|
||||||
Icon={IconCircleOff}
|
Icon={IconCircleOff}
|
||||||
isHighlighted={isHighlighted}
|
isHighlighted={isHighlighted}
|
||||||
/>
|
/>
|
||||||
@ -42,7 +48,7 @@ export const JsonNode = ({
|
|||||||
return (
|
return (
|
||||||
<JsonValueNode
|
<JsonValueNode
|
||||||
label={label}
|
label={label}
|
||||||
valueAsString={value}
|
valueAsString={isNonEmptyString(value) ? value : emptyStringLabel}
|
||||||
Icon={IconTypography}
|
Icon={IconTypography}
|
||||||
isHighlighted={isHighlighted}
|
isHighlighted={isHighlighted}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,20 +1,27 @@
|
|||||||
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
|
import { JsonList } from '@ui/json-visualizer/components/internal/JsonList';
|
||||||
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
|
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
|
||||||
import { JsonTreeContextProvider } from '@ui/json-visualizer/components/JsonTreeContextProvider';
|
import { JsonTreeContextProvider } from '@ui/json-visualizer/components/JsonTreeContextProvider';
|
||||||
|
import { ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||||
import { JsonValue } from 'type-fest';
|
import { JsonValue } from 'type-fest';
|
||||||
|
|
||||||
export const JsonTree = ({
|
export const JsonTree = ({
|
||||||
value,
|
value,
|
||||||
shouldHighlightNode,
|
shouldHighlightNode,
|
||||||
|
shouldExpandNodeInitially,
|
||||||
emptyArrayLabel,
|
emptyArrayLabel,
|
||||||
emptyObjectLabel,
|
emptyObjectLabel,
|
||||||
|
emptyStringLabel,
|
||||||
arrowButtonCollapsedLabel,
|
arrowButtonCollapsedLabel,
|
||||||
arrowButtonExpandedLabel,
|
arrowButtonExpandedLabel,
|
||||||
}: {
|
}: {
|
||||||
value: JsonValue;
|
value: JsonValue;
|
||||||
shouldHighlightNode?: (keyPath: string) => boolean;
|
shouldHighlightNode?: (keyPath: string) => boolean;
|
||||||
|
shouldExpandNodeInitially: (
|
||||||
|
params: ShouldExpandNodeInitiallyProps,
|
||||||
|
) => boolean;
|
||||||
emptyArrayLabel: string;
|
emptyArrayLabel: string;
|
||||||
emptyObjectLabel: string;
|
emptyObjectLabel: string;
|
||||||
|
emptyStringLabel: string;
|
||||||
arrowButtonCollapsedLabel: string;
|
arrowButtonCollapsedLabel: string;
|
||||||
arrowButtonExpandedLabel: string;
|
arrowButtonExpandedLabel: string;
|
||||||
}) => {
|
}) => {
|
||||||
@ -22,8 +29,10 @@ export const JsonTree = ({
|
|||||||
<JsonTreeContextProvider
|
<JsonTreeContextProvider
|
||||||
value={{
|
value={{
|
||||||
shouldHighlightNode,
|
shouldHighlightNode,
|
||||||
|
shouldExpandNodeInitially,
|
||||||
emptyArrayLabel,
|
emptyArrayLabel,
|
||||||
emptyObjectLabel,
|
emptyObjectLabel,
|
||||||
|
emptyStringLabel,
|
||||||
arrowButtonCollapsedLabel,
|
arrowButtonCollapsedLabel,
|
||||||
arrowButtonExpandedLabel,
|
arrowButtonExpandedLabel,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export type ShouldExpandNodeInitiallyProps = { keyPath: string; depth: number };
|
||||||
|
|
||||||
export type JsonTreeContextType = {
|
export type JsonTreeContextType = {
|
||||||
shouldHighlightNode?: (keyPath: string) => boolean;
|
shouldHighlightNode?: (keyPath: string) => boolean;
|
||||||
|
shouldExpandNodeInitially: (
|
||||||
|
params: ShouldExpandNodeInitiallyProps,
|
||||||
|
) => boolean;
|
||||||
|
emptyStringLabel: string;
|
||||||
emptyArrayLabel: string;
|
emptyArrayLabel: string;
|
||||||
emptyObjectLabel: string;
|
emptyObjectLabel: string;
|
||||||
arrowButtonCollapsedLabel: string;
|
arrowButtonCollapsedLabel: string;
|
||||||
|
|||||||
@ -8,3 +8,4 @@ export * from './components/JsonValueNode';
|
|||||||
export * from './contexts/JsonTreeContext';
|
export * from './contexts/JsonTreeContext';
|
||||||
export * from './hooks/useJsonTreeContextOrThrow';
|
export * from './hooks/useJsonTreeContextOrThrow';
|
||||||
export * from './utils/isArray';
|
export * from './utils/isArray';
|
||||||
|
export * from './utils/isTwoFirstDepths';
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||||
|
|
||||||
|
export const isTwoFirstDepths = ({ depth }: ShouldExpandNodeInitiallyProps) =>
|
||||||
|
depth <= 1;
|
||||||
Reference in New Issue
Block a user