Order the workflow run's output properly in the JsonFieldDisplay (#11583)

In this PR:

- Order the workflow run's output in the JsonField Display; the order
should be: error, stepsOutput, flow
- Ensure the special characters are hidden in the JSON visualizer
- Add missing scenarios to Json Tree's stories as it ensures Chromatic
checks them


https://github.com/user-attachments/assets/2ca5ae1d-fdba-4327-bad2-246fd9d23cb9

Closes https://github.com/twentyhq/core-team-issues/issues/804
This commit is contained in:
Baptiste Devessier
2025-04-15 18:40:53 +02:00
committed by GitHub
parent 8bd7b78825
commit c23942ce6f
5 changed files with 157 additions and 23 deletions

View File

@ -0,0 +1,25 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { orderWorkflowRunOutput } from '@/object-record/record-field/meta-types/utils/orderWorkflowRunOutput';
import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useFormattedJsonFieldValue = ({
fieldValue,
}: {
fieldValue: FieldJsonValue | undefined;
}): FieldJsonValue | undefined => {
const { fieldDefinition } = useContext(FieldContext);
if (
fieldDefinition.metadata.objectMetadataNameSingular ===
CoreObjectNameSingular.WorkflowRun &&
fieldDefinition.metadata.fieldName === 'output' &&
isDefined(fieldValue)
) {
return orderWorkflowRunOutput(fieldValue) as FieldJsonValue;
}
return fieldValue;
};

View File

@ -3,6 +3,7 @@ import { useContext } from 'react';
import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata'; import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useFormattedJsonFieldValue } from '@/object-record/record-field/meta-types/hooks/useFormattedJsonFieldValue';
import { FieldContext } from '../../contexts/FieldContext'; import { FieldContext } from '../../contexts/FieldContext';
export const useJsonFieldDisplay = () => { export const useJsonFieldDisplay = () => {
@ -15,9 +16,13 @@ export const useJsonFieldDisplay = () => {
fieldName, fieldName,
); );
const formattedFieldValue = useFormattedJsonFieldValue({
fieldValue,
});
return { return {
maxWidth, maxWidth,
fieldDefinition, fieldDefinition,
fieldValue, fieldValue: formattedFieldValue,
}; };
}; };

View File

@ -1,7 +1,6 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { WorkflowRunOutput } from '@/workflow/types/Workflow'; import { orderWorkflowRunOutput } from '@/object-record/record-field/meta-types/utils/orderWorkflowRunOutput';
import { workflowRunOutputSchema } from '@/workflow/validation-schemas/workflowSchema';
import { useContext } from 'react'; import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { JsonObject, JsonValue } from 'type-fest'; import { JsonObject, JsonValue } from 'type-fest';
@ -22,26 +21,7 @@ export const usePrecomputedJsonDraftValue = ({
fieldDefinition.metadata.fieldName === 'output' && fieldDefinition.metadata.fieldName === 'output' &&
isDefined(draftValue) isDefined(draftValue)
) { ) {
const parsedValue = workflowRunOutputSchema.safeParse(parsedJsonValue); return orderWorkflowRunOutput(parsedJsonValue) as JsonObject;
if (!parsedValue.success) {
return null;
}
const orderedWorkflowRunOutput: WorkflowRunOutput = {
...(isDefined(parsedValue.data.error)
? {
error: parsedValue.data.error,
}
: {}),
...(isDefined(parsedValue.data.stepsOutput)
? {
stepsOutput: parsedValue.data.stepsOutput,
}
: {}),
flow: parsedValue.data.flow,
};
return orderedWorkflowRunOutput as JsonObject;
} }
return parsedJsonValue; return parsedJsonValue;

View File

@ -0,0 +1,27 @@
import { WorkflowRunOutput } from '@/workflow/types/Workflow';
import { workflowRunOutputSchema } from '@/workflow/validation-schemas/workflowSchema';
import { isDefined } from 'twenty-shared/utils';
import { JsonValue } from 'type-fest';
export const orderWorkflowRunOutput = (value: JsonValue) => {
const parsedValue = workflowRunOutputSchema.safeParse(value);
if (!parsedValue.success) {
return null;
}
const orderedWorkflowRunOutput: WorkflowRunOutput = {
...(isDefined(parsedValue.data.error)
? {
error: parsedValue.data.error,
}
: {}),
...(isDefined(parsedValue.data.stepsOutput)
? {
stepsOutput: parsedValue.data.stepsOutput,
}
: {}),
flow: parsedValue.data.flow,
};
return orderedWorkflowRunOutput;
};

View File

@ -32,30 +32,85 @@ export const String: Story = {
args: { args: {
value: 'Hello', value: 'Hello',
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('Hello');
expect(node).toBeVisible();
},
};
export const StringWithSpecialCharacters: Story = {
args: {
value: 'Merry \n Christmas \t 🎄',
onNodeValueClick: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('Merry Christmas 🎄');
await userEvent.click(node);
await waitFor(() => {
expect(args.onNodeValueClick).toHaveBeenCalledWith(
'Merry \n Christmas \t 🎄',
);
});
},
}; };
export const Number: Story = { export const Number: Story = {
args: { args: {
value: 42, value: 42,
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('42');
expect(node).toBeVisible();
},
}; };
export const Boolean: Story = { export const Boolean: Story = {
args: { args: {
value: true, value: true,
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('true');
expect(node).toBeVisible();
},
}; };
export const Null: Story = { export const Null: Story = {
args: { args: {
value: null, value: null,
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('null');
expect(node).toBeVisible();
},
}; };
export const ArraySimple: Story = { export const ArraySimple: Story = {
args: { args: {
value: [1, 2, 3], value: [1, 2, 3],
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const node = await canvas.findByText('[3]');
expect(node).toBeVisible();
},
}; };
export const ArrayEmpty: Story = { export const ArrayEmpty: Story = {
@ -130,6 +185,15 @@ export const ObjectSimple: Story = {
age: 30, age: 30,
}, },
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const name = await canvas.findByText('John Doe');
expect(name).toBeVisible();
const age = await canvas.findByText('30');
expect(age).toBeVisible();
},
}; };
export const ObjectEmpty: Story = { export const ObjectEmpty: Story = {
@ -199,6 +263,15 @@ export const ObjectWithArray: Story = {
}, },
}, },
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const nestedArrayCount = await canvas.findByText('[2]');
expect(nestedArrayCount).toBeVisible();
const nestedObjectCounts = await canvas.findAllByText('{2}');
expect(nestedObjectCounts).toHaveLength(3);
},
}; };
export const NestedElementCanBeCollapsed: Story = { export const NestedElementCanBeCollapsed: Story = {
@ -422,6 +495,15 @@ export const ReallyDeepNestedObject: Story = {
}, },
}, },
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const finalNodes = await canvas.findAllByText('end');
expect(finalNodes).toHaveLength(2);
expect(finalNodes[0]).toBeVisible();
expect(finalNodes[1]).toBeVisible();
},
}; };
export const LongText: Story = { export const LongText: Story = {
@ -431,6 +513,21 @@ export const LongText: Story = {
'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.', 'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.',
}, },
}, },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const label = await canvas.findByText(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum iaculis est tincidunt, sagittis neque vitae, sodales purus.',
);
expect(label).toBeVisible();
const value = await canvas.findByText(
'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.',
);
expect(value).toBeVisible();
},
}; };
export const BlueHighlighting: Story = { export const BlueHighlighting: Story = {