From c23942ce6f2bb8e854a9fc3bb7916d4882553cd0 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Tue, 15 Apr 2025 18:40:53 +0200 Subject: [PATCH] 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 --- .../hooks/useFormattedJsonFieldValue.ts | 25 +++++ .../meta-types/hooks/useJsonFieldDisplay.ts | 7 +- .../hooks/usePrecomputedJsonDraftValue.ts | 24 +---- .../utils/orderWorkflowRunOutput.ts | 27 ++++++ .../__stories__/JsonTree.stories.tsx | 97 +++++++++++++++++++ 5 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useFormattedJsonFieldValue.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/meta-types/utils/orderWorkflowRunOutput.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useFormattedJsonFieldValue.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useFormattedJsonFieldValue.ts new file mode 100644 index 000000000..44a0365b4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useFormattedJsonFieldValue.ts @@ -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; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonFieldDisplay.ts index 8453736b9..765225fb5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useJsonFieldDisplay.ts @@ -3,6 +3,7 @@ import { useContext } from 'react'; import { FieldJsonValue } from '@/object-record/record-field/types/FieldMetadata'; 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'; export const useJsonFieldDisplay = () => { @@ -15,9 +16,13 @@ export const useJsonFieldDisplay = () => { fieldName, ); + const formattedFieldValue = useFormattedJsonFieldValue({ + fieldValue, + }); + return { maxWidth, fieldDefinition, - fieldValue, + fieldValue: formattedFieldValue, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePrecomputedJsonDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePrecomputedJsonDraftValue.ts index ce9a0b10b..df6c772e0 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePrecomputedJsonDraftValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePrecomputedJsonDraftValue.ts @@ -1,7 +1,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; -import { WorkflowRunOutput } from '@/workflow/types/Workflow'; -import { workflowRunOutputSchema } from '@/workflow/validation-schemas/workflowSchema'; +import { orderWorkflowRunOutput } from '@/object-record/record-field/meta-types/utils/orderWorkflowRunOutput'; import { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { JsonObject, JsonValue } from 'type-fest'; @@ -22,26 +21,7 @@ export const usePrecomputedJsonDraftValue = ({ fieldDefinition.metadata.fieldName === 'output' && isDefined(draftValue) ) { - const parsedValue = workflowRunOutputSchema.safeParse(parsedJsonValue); - 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 orderWorkflowRunOutput(parsedJsonValue) as JsonObject; } return parsedJsonValue; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/utils/orderWorkflowRunOutput.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/utils/orderWorkflowRunOutput.ts new file mode 100644 index 000000000..77ce4deb5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/utils/orderWorkflowRunOutput.ts @@ -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; +}; diff --git a/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx b/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx index 9b5b90b4a..d2a1df6d0 100644 --- a/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx +++ b/packages/twenty-ui/src/json-visualizer/__stories__/JsonTree.stories.tsx @@ -32,30 +32,85 @@ export const String: Story = { args: { 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 = { args: { value: 42, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('42'); + + expect(node).toBeVisible(); + }, }; export const Boolean: Story = { args: { value: true, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('true'); + + expect(node).toBeVisible(); + }, }; export const Null: Story = { args: { value: null, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('null'); + + expect(node).toBeVisible(); + }, }; export const ArraySimple: Story = { args: { value: [1, 2, 3], }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const node = await canvas.findByText('[3]'); + + expect(node).toBeVisible(); + }, }; export const ArrayEmpty: Story = { @@ -130,6 +185,15 @@ export const ObjectSimple: Story = { 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 = { @@ -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 = { @@ -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 = { @@ -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.', }, }, + 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 = {