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 { IconBrackets, useIcons } from 'twenty-ui/display';
import {
GetJsonNodeHighlighting,
JsonNestedNode,
JsonTreeContextProvider,
ShouldExpandNodeInitiallyProps,
@ -70,6 +71,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const variablesUsedInStep = getWorkflowVariablesUsedInStep({
step,
});
const allVariablesUsedInStep = Array.from(variablesUsedInStep);
const stepContext = getWorkflowRunStepContext({
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.');
}
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 = ({
keyPath,
depth,
@ -104,8 +121,7 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
emptyStringLabel: t`[empty string]`,
arrowButtonCollapsedLabel: t`Expand`,
arrowButtonExpandedLabel: t`Collapse`,
getNodeHighlighting: (keyPath) =>
variablesUsedInStep.has(keyPath) ? 'blue' : undefined,
getNodeHighlighting,
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 = {
args: {
value: {

View File

@ -1,6 +1,7 @@
import { IconBrackets } from '@ui/display';
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { JsonArray } from 'type-fest';
export const JsonArrayNode = ({
@ -8,11 +9,13 @@ export const JsonArrayNode = ({
value,
depth,
keyPath,
highlighting,
}: {
label?: string;
value: JsonArray;
depth: number;
keyPath: string;
highlighting: JsonNodeHighlighting | undefined;
}) => {
const { emptyArrayLabel } = useJsonTreeContextOrThrow();
@ -29,6 +32,7 @@ export const JsonArrayNode = ({
depth={depth}
emptyElementsText={emptyArrayLabel}
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 { JsonNode } from '@ui/json-visualizer/components/JsonNode';
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
import { ANIMATION } from '@ui/theme';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
import { JsonValue } from 'type-fest';
import { isDefined } from 'twenty-shared/utils';
import { JsonValue } from 'type-fest';
const StyledContainer = styled.li`
display: grid;
@ -41,6 +42,7 @@ export const JsonNestedNode = ({
emptyElementsText,
depth,
keyPath,
highlighting,
}: {
label?: string;
Icon: IconComponent;
@ -49,6 +51,7 @@ export const JsonNestedNode = ({
emptyElementsText: string;
depth: number;
keyPath: string;
highlighting?: JsonNodeHighlighting | undefined;
}) => {
const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow();
@ -115,7 +118,11 @@ export const JsonNestedNode = ({
return (
<StyledContainer>
<StyledLabelContainer>
<JsonArrow isOpen={isOpen} onClick={handleArrowClick} />
<JsonArrow
isOpen={isOpen}
onClick={handleArrowClick}
variant={highlighting === 'partial-blue' ? 'blue' : undefined}
/>
<JsonNodeLabel label={label} Icon={Icon} />

View File

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

View File

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

View File

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

View File

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

View File

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