Allow json in workflow run's error field (#12762)

We can now inspect errors even if they contain complex data as objects.
Only the first line of the error is put in red.



![CleanShot 2025-06-20 at 18 31
54@2x](https://github.com/user-attachments/assets/a3fd41fb-0063-4fe1-8185-54137c2a0d6e)


![image](https://github.com/user-attachments/assets/833a2851-e7d5-4985-9e42-07a1899cd3de)
This commit is contained in:
Baptiste Devessier
2025-06-20 19:07:24 +02:00
committed by GitHub
parent 1e0ee9421d
commit 22e126869c
6 changed files with 55 additions and 19 deletions

View File

@ -3,6 +3,7 @@ import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { WorkflowRun } from '@/workflow/types/Workflow'; import { WorkflowRun } from '@/workflow/types/Workflow';
import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema'; import { workflowRunSchema } from '@/workflow/validation-schemas/workflowSchema';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useWorkflowRun = ({ export const useWorkflowRun = ({
workflowRunId, workflowRunId,
@ -14,13 +15,18 @@ export const useWorkflowRun = ({
objectRecordId: workflowRunId, objectRecordId: workflowRunId,
}); });
const { success, data: record } = useMemo( const {
() => workflowRunSchema.safeParse(rawRecord), success,
[rawRecord], data: record,
); error,
} = useMemo(() => workflowRunSchema.safeParse(rawRecord), [rawRecord]);
if (!isDefined(rawRecord)) {
return undefined;
}
if (!success) { if (!success) {
return undefined; throw error;
} }
return record; return record;

View File

@ -271,7 +271,7 @@ export const workflowTriggerSchema = z.discriminatedUnion('type', [
// Step output schemas // Step output schemas
export const workflowExecutorOutputSchema = z.object({ export const workflowExecutorOutputSchema = z.object({
result: z.any().optional(), result: z.any().optional(),
error: z.string().optional(), error: z.any().optional(),
pendingEvent: z.boolean().optional(), pendingEvent: z.boolean().optional(),
}); });
@ -286,7 +286,7 @@ export const workflowRunOutputSchema = z.object({
steps: z.array(workflowActionSchema), steps: z.array(workflowActionSchema),
}), }),
stepsOutput: workflowRunOutputStepsOutputSchema.optional(), stepsOutput: workflowRunOutputStepsOutputSchema.optional(),
error: z.string().optional(), error: z.any().optional(),
}); });
export const workflowRunContextSchema = z.record(z.any()); export const workflowRunContextSchema = z.record(z.any());

View File

@ -67,7 +67,13 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
? getTriggerHeaderType(stepDefinition.definition) ? getTriggerHeaderType(stepDefinition.definition)
: i18n._(getActionHeaderTypeOrThrow(stepDefinition.definition.type)); : i18n._(getActionHeaderTypeOrThrow(stepDefinition.definition.type));
const setRedHighlightingForEveryNode: GetJsonNodeHighlighting = () => 'red'; const setRedHighlightingForEveryNode: GetJsonNodeHighlighting = (keyPath) => {
if (keyPath === 'error') {
return 'red';
}
return undefined;
};
return ( return (
<> <>

View File

@ -571,6 +571,9 @@ export const RedHighlighting: Story = {
value: { value: {
name: 'John Doe', name: 'John Doe',
age: 30, age: 30,
address: {
city: 'Paris',
},
}, },
getNodeHighlighting: () => 'red', getNodeHighlighting: () => 'red',
}, },

View File

@ -25,8 +25,9 @@ const StyledLabelContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledElementsCount = styled.span` const StyledElementsCount = styled.span<{ variant?: 'red' }>`
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme, variant }) =>
variant === 'red' ? theme.font.color.danger : theme.font.color.tertiary};
`; `;
const StyledJsonList = styled(JsonList)``.withComponent(motion.ul); const StyledJsonList = styled(JsonList)``.withComponent(motion.ul);
@ -118,13 +119,25 @@ export const JsonNestedNode = ({
<JsonArrow <JsonArrow
isOpen={isOpen} isOpen={isOpen}
onClick={handleArrowClick} onClick={handleArrowClick}
variant={highlighting === 'partial-blue' ? 'blue' : undefined} variant={
highlighting === 'partial-blue'
? 'blue'
: highlighting === 'red'
? highlighting
: undefined
}
/> />
<JsonNodeLabel label={label} Icon={Icon} /> <JsonNodeLabel
label={label}
Icon={Icon}
highlighting={highlighting === 'red' ? highlighting : undefined}
/>
{renderElementsCount && ( {renderElementsCount && (
<StyledElementsCount> <StyledElementsCount
variant={highlighting === 'red' ? 'red' : undefined}
>
{renderElementsCount(elements.length)} {renderElementsCount(elements.length)}
</StyledElementsCount> </StyledElementsCount>
)} )}

View File

@ -6,16 +6,20 @@ import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTree
import { ANIMATION } from '@ui/theme'; import { ANIMATION } from '@ui/theme';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
const StyledButton = styled(motion.button)` const StyledButton = styled(motion.button)<{ variant?: 'blue' | 'red' }>`
align-items: center; align-items: center;
border-color: ${({ theme }) => theme.border.color.medium}; background-color: ${({ theme, variant }) =>
variant === 'red'
? theme.background.danger
: theme.background.transparent.lighter};
border-color: ${({ theme, variant }) =>
variant === 'red' ? theme.border.color.danger : theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding-inline: ${({ theme }) => theme.spacing(1)}; padding-inline: ${({ theme }) => theme.spacing(1)};
background-color: ${({ theme }) => theme.background.transparent.lighter};
height: 24px; height: 24px;
width: 24px; width: 24px;
box-sizing: border-box; box-sizing: border-box;
@ -31,7 +35,7 @@ export const JsonArrow = ({
}: { }: {
isOpen: boolean; isOpen: boolean;
onClick: () => void; onClick: () => void;
variant?: 'blue'; variant?: 'blue' | 'red';
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@ -39,7 +43,7 @@ export const JsonArrow = ({
useJsonTreeContextOrThrow(); useJsonTreeContextOrThrow();
return ( return (
<StyledButton onClick={onClick}> <StyledButton variant={variant} onClick={onClick}>
<VisibilityHidden> <VisibilityHidden>
{isOpen ? arrowButtonExpandedLabel : arrowButtonCollapsedLabel} {isOpen ? arrowButtonExpandedLabel : arrowButtonCollapsedLabel}
</VisibilityHidden> </VisibilityHidden>
@ -47,7 +51,11 @@ export const JsonArrow = ({
<MotionIconChevronDown <MotionIconChevronDown
size={theme.icon.size.md} size={theme.icon.size.md}
color={ color={
variant === 'blue' ? theme.color.blue : theme.font.color.secondary variant === 'blue'
? theme.color.blue
: variant === 'red'
? theme.font.color.danger
: theme.font.color.secondary
} }
initial={false} initial={false}
animate={{ rotate: isOpen ? 0 : -90 }} animate={{ rotate: isOpen ? 0 : -90 }}