JSON visualizer: Highlight the parent nodes of in-use nodes (#11373)

https://github.com/user-attachments/assets/5f31023d-b24f-40c8-a061-ffc0d02b63b0

Closes https://github.com/twentyhq/core-team-issues/issues/715
This commit is contained in:
Baptiste Devessier
2025-04-03 12:11:19 +02:00
committed by GitHub
parent 4a4e65fe4a
commit 144a326709
9 changed files with 66 additions and 7 deletions

View File

@ -14,6 +14,7 @@ import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { IconBrackets, useIcons } from 'twenty-ui/display'; import { IconBrackets, useIcons } from 'twenty-ui/display';
import { import {
GetJsonNodeHighlighting,
JsonNestedNode, JsonNestedNode,
JsonTreeContextProvider, JsonTreeContextProvider,
ShouldExpandNodeInitiallyProps, ShouldExpandNodeInitiallyProps,
@ -70,6 +71,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const variablesUsedInStep = getWorkflowVariablesUsedInStep({ const variablesUsedInStep = getWorkflowVariablesUsedInStep({
step, step,
}); });
const allVariablesUsedInStep = Array.from(variablesUsedInStep);
const stepContext = getWorkflowRunStepContext({ const stepContext = getWorkflowRunStepContext({
context: workflowRun.context, context: workflowRun.context,
@ -80,6 +82,21 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
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 getNodeHighlighting: GetJsonNodeHighlighting = (keyPath: string) => {
if (variablesUsedInStep.has(keyPath)) {
return 'blue';
}
const isUsedVariableParent = allVariablesUsedInStep.some((variable) =>
variable.startsWith(keyPath),
);
if (isUsedVariableParent) {
return 'partial-blue';
}
return undefined;
};
const isFirstNodeDepthOfPreviousStep = ({ const isFirstNodeDepthOfPreviousStep = ({
keyPath, keyPath,
depth, depth,
@ -104,8 +121,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
emptyStringLabel: t`[empty string]`, emptyStringLabel: t`[empty string]`,
arrowButtonCollapsedLabel: t`Expand`, arrowButtonCollapsedLabel: t`Expand`,
arrowButtonExpandedLabel: t`Collapse`, arrowButtonExpandedLabel: t`Collapse`,
getNodeHighlighting: (keyPath) => getNodeHighlighting,
variablesUsedInStep.has(keyPath) ? 'blue' : undefined,
shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep, shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep,
}} }}
> >

View File

@ -447,6 +447,26 @@ export const BlueHighlighting: Story = {
}, },
}; };
export const PartialBlueHighlighting: Story = {
args: {
value: {
name: 'John Doe',
age: 30,
address: {
city: 'Paris',
},
},
getNodeHighlighting: (keyPath: string) =>
keyPath === 'address' ? 'partial-blue' : undefined,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const ageElement = await canvas.findByText('age');
expect(ageElement).toBeVisible();
},
};
export const RedHighlighting: Story = { export const RedHighlighting: Story = {
args: { args: {
value: { value: {

View File

@ -1,6 +1,7 @@
import { IconBrackets } from '@ui/display'; import { IconBrackets } from '@ui/display';
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode'; import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { JsonArray } from 'type-fest'; import { JsonArray } from 'type-fest';
export const JsonArrayNode = ({ export const JsonArrayNode = ({
@ -8,11 +9,13 @@ export const JsonArrayNode = ({
value, value,
depth, depth,
keyPath, keyPath,
highlighting,
}: { }: {
label?: string; label?: string;
value: JsonArray; value: JsonArray;
depth: number; depth: number;
keyPath: string; keyPath: string;
highlighting: JsonNodeHighlighting | undefined;
}) => { }) => {
const { emptyArrayLabel } = useJsonTreeContextOrThrow(); const { emptyArrayLabel } = useJsonTreeContextOrThrow();
@ -29,6 +32,7 @@ export const JsonArrayNode = ({
depth={depth} depth={depth}
emptyElementsText={emptyArrayLabel} emptyElementsText={emptyArrayLabel}
keyPath={keyPath} keyPath={keyPath}
highlighting={highlighting}
/> />
); );
}; };

View File

@ -6,11 +6,12 @@ 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 { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
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';
import { JsonValue } from 'type-fest';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { JsonValue } from 'type-fest';
const StyledContainer = styled.li` const StyledContainer = styled.li`
display: grid; display: grid;
@ -41,6 +42,7 @@ export const JsonNestedNode = ({
emptyElementsText, emptyElementsText,
depth, depth,
keyPath, keyPath,
highlighting,
}: { }: {
label?: string; label?: string;
Icon: IconComponent; Icon: IconComponent;
@ -49,6 +51,7 @@ export const JsonNestedNode = ({
emptyElementsText: string; emptyElementsText: string;
depth: number; depth: number;
keyPath: string; keyPath: string;
highlighting?: JsonNodeHighlighting | undefined;
}) => { }) => {
const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow(); const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow();
@ -115,7 +118,11 @@ export const JsonNestedNode = ({
return ( return (
<StyledContainer> <StyledContainer>
<StyledLabelContainer> <StyledLabelContainer>
<JsonArrow isOpen={isOpen} onClick={handleArrowClick} /> <JsonArrow
isOpen={isOpen}
onClick={handleArrowClick}
variant={highlighting === 'partial-blue' ? 'blue' : undefined}
/>
<JsonNodeLabel label={label} Icon={Icon} /> <JsonNodeLabel label={label} Icon={Icon} />

View File

@ -84,6 +84,7 @@ export const JsonNode = ({
value={value} value={value}
depth={depth} depth={depth}
keyPath={keyPath} keyPath={keyPath}
highlighting={highlighting}
/> />
); );
} }
@ -94,6 +95,7 @@ export const JsonNode = ({
value={value} value={value}
depth={depth} depth={depth}
keyPath={keyPath} keyPath={keyPath}
highlighting={highlighting}
/> />
); );
}; };

View File

@ -1,6 +1,7 @@
import { IconCube } from '@ui/display'; import { IconCube } from '@ui/display';
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode'; import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow'; import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { JsonObject } from 'type-fest'; import { JsonObject } from 'type-fest';
export const JsonObjectNode = ({ export const JsonObjectNode = ({
@ -8,11 +9,13 @@ export const JsonObjectNode = ({
value, value,
depth, depth,
keyPath, keyPath,
highlighting,
}: { }: {
label?: string; label?: string;
value: JsonObject; value: JsonObject;
depth: number; depth: number;
keyPath: string; keyPath: string;
highlighting: JsonNodeHighlighting | undefined;
}) => { }) => {
const { emptyObjectLabel } = useJsonTreeContextOrThrow(); const { emptyObjectLabel } = useJsonTreeContextOrThrow();
@ -29,6 +32,7 @@ export const JsonObjectNode = ({
depth={depth} depth={depth}
emptyElementsText={emptyObjectLabel} emptyElementsText={emptyObjectLabel}
keyPath={keyPath} keyPath={keyPath}
highlighting={highlighting}
/> />
); );
}; };

View File

@ -27,9 +27,11 @@ const MotionIconChevronDown = motion.create(IconChevronDown);
export const JsonArrow = ({ export const JsonArrow = ({
isOpen, isOpen,
onClick, onClick,
variant,
}: { }: {
isOpen: boolean; isOpen: boolean;
onClick: () => void; onClick: () => void;
variant?: 'blue';
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@ -44,7 +46,9 @@ export const JsonArrow = ({
<MotionIconChevronDown <MotionIconChevronDown
size={theme.icon.size.md} size={theme.icon.size.md}
color={theme.font.color.secondary} color={
variant === 'blue' ? theme.color.blue : theme.font.color.secondary
}
initial={false} initial={false}
animate={{ rotate: isOpen ? 0 : -90 }} animate={{ rotate: isOpen ? 0 : -90 }}
transition={{ duration: ANIMATION.duration.normal }} transition={{ duration: ANIMATION.duration.normal }}

View File

@ -8,7 +8,7 @@ const StyledText = styled.span<{
highlighting === 'blue' highlighting === 'blue'
? theme.adaptiveColors.blue4 ? theme.adaptiveColors.blue4
: highlighting === 'red' : highlighting === 'red'
? theme.font.color.danger ? theme.adaptiveColors.red4
: theme.font.color.tertiary}; : theme.font.color.tertiary};
`; `;

View File

@ -1,3 +1,5 @@
import { ThemeColor } from '@ui/theme'; import { ThemeColor } from '@ui/theme';
export type JsonNodeHighlighting = Extract<ThemeColor, 'blue' | 'red'>; export type JsonNodeHighlighting =
| Extract<ThemeColor, 'blue' | 'red'>
| 'partial-blue';