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}
|
||||
|
||||
{activeTabId === 'input' ? (
|
||||
<WorkflowRunStepInputDetail stepId={workflowSelectedNode} />
|
||||
<WorkflowRunStepInputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === 'output' ? (
|
||||
<WorkflowRunStepOutputDetail stepId={workflowSelectedNode} />
|
||||
<WorkflowRunStepOutputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
</WorkflowStepContextProvider>
|
||||
);
|
||||
|
||||
@ -33,6 +33,8 @@ export const JsonDataIndicatorHealthStatus = () => {
|
||||
!indicatorHealth.status ||
|
||||
indicatorHealth.status === AdminPanelHealthServiceStatus.OUTAGE;
|
||||
|
||||
const isAnyNode = () => true;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
{isDown && (
|
||||
@ -45,8 +47,10 @@ export const JsonDataIndicatorHealthStatus = () => {
|
||||
<StyledDetailsContainer>
|
||||
<JsonTree
|
||||
value={parsedDetails}
|
||||
shouldExpandNodeInitially={isAnyNode}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`Empty Object`}
|
||||
emptyStringLabel={t`[empty string]`}
|
||||
arrowButtonCollapsedLabel={t`Expand`}
|
||||
arrowButtonExpandedLabel={t`Collapse`}
|
||||
/>
|
||||
|
||||
@ -85,11 +85,17 @@ export const RightDrawerWorkflowRunViewStep = () => {
|
||||
) : null}
|
||||
|
||||
{activeTabId === 'input' ? (
|
||||
<WorkflowRunStepInputDetail stepId={workflowSelectedNode} />
|
||||
<WorkflowRunStepInputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTabId === 'output' ? (
|
||||
<WorkflowRunStepOutputDetail stepId={workflowSelectedNode} />
|
||||
<WorkflowRunStepOutputDetail
|
||||
key={workflowSelectedNode}
|
||||
stepId={workflowSelectedNode}
|
||||
/>
|
||||
) : null}
|
||||
</WorkflowStepContextProvider>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getWorkflowPreviousStepId } from '@/workflow/workflow-steps/utils/getWorkflowPreviousStep';
|
||||
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
|
||||
import styled from '@emotion/styled';
|
||||
@ -9,6 +10,7 @@ import {
|
||||
IconBrackets,
|
||||
JsonNestedNode,
|
||||
JsonTreeContextProvider,
|
||||
ShouldExpandNodeInitiallyProps,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -38,6 +40,15 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previousStepId = getWorkflowPreviousStepId({
|
||||
stepId,
|
||||
steps: workflowRun.output.flow.steps,
|
||||
});
|
||||
|
||||
if (previousStepId === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const variablesUsedInStep = getWorkflowVariablesUsedInStep({
|
||||
step,
|
||||
});
|
||||
@ -47,20 +58,27 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
flow: workflowRun.output.flow,
|
||||
stepId,
|
||||
});
|
||||
|
||||
if (stepContext.length === 0) {
|
||||
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 (
|
||||
<StyledContainer>
|
||||
<JsonTreeContextProvider
|
||||
value={{
|
||||
emptyArrayLabel: t`Empty Array`,
|
||||
emptyObjectLabel: t`Empty Object`,
|
||||
emptyStringLabel: t`[empty string]`,
|
||||
arrowButtonCollapsedLabel: t`Expand`,
|
||||
arrowButtonExpandedLabel: t`Collapse`,
|
||||
shouldHighlightNode: (keyPath) => variablesUsedInStep.has(keyPath),
|
||||
shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep,
|
||||
}}
|
||||
>
|
||||
<JsonNestedNode
|
||||
|
||||
@ -3,7 +3,7 @@ import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThro
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { JsonTree } from 'twenty-ui';
|
||||
import { isTwoFirstDepths, JsonTree } from 'twenty-ui';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: grid;
|
||||
@ -28,8 +28,10 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
<StyledContainer>
|
||||
<JsonTree
|
||||
value={stepOutput}
|
||||
shouldExpandNodeInitially={isTwoFirstDepths}
|
||||
emptyArrayLabel={t`Empty Array`}
|
||||
emptyObjectLabel={t`Empty Object`}
|
||||
emptyStringLabel={t`[empty string]`}
|
||||
arrowButtonCollapsedLabel={t`Expand`}
|
||||
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,
|
||||
} 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: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -8,3 +8,4 @@ export * from './components/JsonValueNode';
|
||||
export * from './contexts/JsonTreeContext';
|
||||
export * from './hooks/useJsonTreeContextOrThrow';
|
||||
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