Set failed node's output as red (#11358)

| Error | Success |
|--------|--------|
| ![CleanShot 2025-04-02 at 18 18
45@2x](https://github.com/user-attachments/assets/6674d4d2-344a-4e16-9608-a70cde07a376)
| ![CleanShot 2025-04-02 at 18 20
23@2x](https://github.com/user-attachments/assets/55b5a467-528f-4f07-9166-40ed14943ee2)
|

Closes https://github.com/twentyhq/core-team-issues/issues/716
This commit is contained in:
Baptiste Devessier
2025-04-03 08:58:56 +02:00
committed by GitHub
parent 183dc40916
commit bea75b9532
12 changed files with 119 additions and 31 deletions

View File

@ -11,6 +11,7 @@ import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import {
IconBrackets,
JsonNestedNode,
@ -18,7 +19,6 @@ import {
ShouldExpandNodeInitiallyProps,
useIcons,
} from 'twenty-ui';
import { isDefined } from 'twenty-shared/utils';
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
const { t, i18n } = useLingui();
@ -105,7 +105,8 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
emptyStringLabel: t`[empty string]`,
arrowButtonCollapsedLabel: t`Expand`,
arrowButtonExpandedLabel: t`Collapse`,
shouldHighlightNode: (keyPath) => variablesUsedInStep.has(keyPath),
getNodeHighlighting: (keyPath) =>
variablesUsedInStep.has(keyPath) ? 'blue' : undefined,
shouldExpandNodeInitially: isFirstNodeDepthOfPreviousStep,
}}
>

View File

@ -9,7 +9,12 @@ import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-ac
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { isTwoFirstDepths, JsonTree, useIcons } from 'twenty-ui';
import {
GetJsonNodeHighlighting,
isTwoFirstDepths,
JsonTree,
useIcons,
} from 'twenty-ui';
export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
const { t, i18n } = useLingui();
@ -42,6 +47,8 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
});
const headerType = getActionHeaderTypeOrThrow(stepDefinition.definition.type);
const setRedHighlightingForEveryNode: GetJsonNodeHighlighting = () => 'red';
return (
<>
<WorkflowStepHeader
@ -61,6 +68,11 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
emptyStringLabel={t`[empty string]`}
arrowButtonCollapsedLabel={t`Expand`}
arrowButtonExpandedLabel={t`Collapse`}
getNodeHighlighting={
isDefined(stepOutput.error)
? setRedHighlightingForEveryNode
: undefined
}
/>
</WorkflowRunStepJsonContainer>
</>

View File

@ -430,3 +430,35 @@ export const LongText: Story = {
},
},
};
export const BlueHighlighting: Story = {
args: {
value: {
name: 'John Doe',
age: 30,
},
getNodeHighlighting: () => 'blue',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const ageElement = await canvas.findByText('age');
expect(ageElement).toBeVisible();
},
};
export const RedHighlighting: Story = {
args: {
value: {
name: 'John Doe',
age: 30,
},
getNodeHighlighting: () => 'red',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const ageElement = await canvas.findByText('age');
expect(ageElement).toBeVisible();
},
};

View File

@ -29,9 +29,9 @@ export const JsonNode = ({
depth: number;
keyPath: string;
}) => {
const { shouldHighlightNode, emptyStringLabel } = useJsonTreeContextOrThrow();
const { getNodeHighlighting, emptyStringLabel } = useJsonTreeContextOrThrow();
const isHighlighted = shouldHighlightNode?.(keyPath) ?? false;
const highlighting = getNodeHighlighting?.(keyPath);
if (isNull(value)) {
return (
@ -39,7 +39,7 @@ export const JsonNode = ({
label={label}
valueAsString="null"
Icon={IconCircleOff}
isHighlighted={isHighlighted}
highlighting={highlighting}
/>
);
}
@ -50,7 +50,7 @@ export const JsonNode = ({
label={label}
valueAsString={isNonEmptyString(value) ? value : emptyStringLabel}
Icon={IconTypography}
isHighlighted={isHighlighted}
highlighting={highlighting}
/>
);
}
@ -61,7 +61,7 @@ export const JsonNode = ({
label={label}
valueAsString={String(value)}
Icon={IconNumber9}
isHighlighted={isHighlighted}
highlighting={highlighting}
/>
);
}
@ -72,7 +72,7 @@ export const JsonNode = ({
label={label}
valueAsString={String(value)}
Icon={IconCheckbox}
isHighlighted={isHighlighted}
highlighting={highlighting}
/>
);
}

View File

@ -2,11 +2,12 @@ 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 { GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting';
import { JsonValue } from 'type-fest';
export const JsonTree = ({
value,
shouldHighlightNode,
getNodeHighlighting,
shouldExpandNodeInitially,
emptyArrayLabel,
emptyObjectLabel,
@ -15,7 +16,7 @@ export const JsonTree = ({
arrowButtonExpandedLabel,
}: {
value: JsonValue;
shouldHighlightNode?: (keyPath: string) => boolean;
getNodeHighlighting?: GetJsonNodeHighlighting;
shouldExpandNodeInitially: (
params: ShouldExpandNodeInitiallyProps,
) => boolean;
@ -28,7 +29,7 @@ export const JsonTree = ({
return (
<JsonTreeContextProvider
value={{
shouldHighlightNode,
getNodeHighlighting,
shouldExpandNodeInitially,
emptyArrayLabel,
emptyObjectLabel,

View File

@ -3,6 +3,7 @@ import { IconComponent } from '@ui/display';
import { JsonListItem } from '@ui/json-visualizer/components/internal/JsonListItem';
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue';
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
const StyledListItem = styled(JsonListItem)`
column-gap: ${({ theme }) => theme.spacing(2)};
@ -10,7 +11,7 @@ const StyledListItem = styled(JsonListItem)`
type JsonValueNodeProps = {
valueAsString: string;
isHighlighted: boolean;
highlighting: JsonNodeHighlighting | undefined;
} & (
| {
label: string;
@ -29,13 +30,13 @@ export const JsonValueNode = (props: JsonValueNodeProps) => {
<JsonNodeLabel
label={props.label}
Icon={props.Icon}
isHighlighted={props.isHighlighted}
highlighting={props.highlighting}
/>
)}
<JsonNodeValue
valueAsString={props.valueAsString}
isHighlighted={props.isHighlighted}
highlighting={props.highlighting}
/>
</StyledListItem>
);

View File

@ -1,13 +1,30 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@ui/display';
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
const StyledLabelContainer = styled.span<{ isHighlighted?: boolean }>`
const StyledLabelContainer = styled.span<{
highlighting?: JsonNodeHighlighting;
}>`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.lighter};
border-color: ${({ theme }) => theme.border.color.medium};
color: ${({ theme, isHighlighted }) =>
isHighlighted ? theme.color.blue : theme.font.color.primary};
background-color: ${({ theme, highlighting }) =>
highlighting === 'blue'
? theme.adaptiveColors.blue1
: highlighting === 'red'
? theme.background.danger
: theme.background.transparent.lighter};
border-color: ${({ theme, highlighting }) =>
highlighting === 'blue'
? theme.adaptiveColors.blue2
: highlighting === 'red'
? theme.border.color.danger
: theme.border.color.medium};
color: ${({ theme, highlighting }) =>
highlighting === 'blue'
? theme.color.blue
: highlighting === 'red'
? theme.font.color.danger
: theme.font.color.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-style: solid;
border-width: 1px;
@ -25,19 +42,25 @@ const StyledLabelContainer = styled.span<{ isHighlighted?: boolean }>`
export const JsonNodeLabel = ({
label,
Icon,
isHighlighted,
highlighting,
}: {
label: string;
Icon: IconComponent;
isHighlighted?: boolean;
highlighting?: JsonNodeHighlighting | undefined;
}) => {
const theme = useTheme();
return (
<StyledLabelContainer isHighlighted={isHighlighted}>
<StyledLabelContainer highlighting={highlighting}>
<Icon
size={theme.icon.size.md}
color={isHighlighted ? theme.color.blue : theme.font.color.tertiary}
color={
highlighting === 'blue'
? theme.color.blue
: highlighting === 'red'
? theme.font.color.danger
: theme.font.color.tertiary
}
/>
<span>{label}</span>

View File

@ -1,16 +1,23 @@
import styled from '@emotion/styled';
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
const StyledText = styled.span<{ isHighlighted?: boolean }>`
color: ${({ theme, isHighlighted }) =>
isHighlighted ? theme.adaptiveColors.blue4 : theme.font.color.tertiary};
const StyledText = styled.span<{
highlighting: JsonNodeHighlighting | undefined;
}>`
color: ${({ theme, highlighting }) =>
highlighting === 'blue'
? theme.adaptiveColors.blue4
: highlighting === 'red'
? theme.font.color.danger
: theme.font.color.tertiary};
`;
export const JsonNodeValue = ({
valueAsString,
isHighlighted,
highlighting,
}: {
valueAsString: string;
isHighlighted?: boolean;
highlighting?: JsonNodeHighlighting | undefined;
}) => {
return <StyledText isHighlighted={isHighlighted}>{valueAsString}</StyledText>;
return <StyledText highlighting={highlighting}>{valueAsString}</StyledText>;
};

View File

@ -1,9 +1,10 @@
import { GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting';
import { createContext } from 'react';
export type ShouldExpandNodeInitiallyProps = { keyPath: string; depth: number };
export type JsonTreeContextType = {
shouldHighlightNode?: (keyPath: string) => boolean;
getNodeHighlighting?: GetJsonNodeHighlighting;
shouldExpandNodeInitially: (
params: ShouldExpandNodeInitiallyProps,
) => boolean;

View File

@ -7,5 +7,7 @@ export * from './components/JsonTreeContextProvider';
export * from './components/JsonValueNode';
export * from './contexts/JsonTreeContext';
export * from './hooks/useJsonTreeContextOrThrow';
export * from './types/GetJsonNodeHighlighting';
export * from './types/JsonNodeHighlighting';
export * from './utils/isArray';
export * from './utils/isTwoFirstDepths';

View File

@ -0,0 +1,5 @@
import { JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
export type GetJsonNodeHighlighting = (
keyPath: string,
) => JsonNodeHighlighting | undefined;

View File

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