Handle JSON viewer empty states (#10750)
- Display the number of descendants for object and array elements - Display an empty state for arrays and objects - Make the input and output visualizer scrollable horizontally - Prevent JSON visualizer's text to wrap ## Demo: input https://github.com/user-attachments/assets/d6bd6acf-a779-4fc7-a8b1-12b857cee7f9 Closes https://github.com/twentyhq/core-team-issues/issues/497
This commit is contained in:
committed by
GitHub
parent
dc55fac1d5
commit
dd26001372
@ -6,7 +6,6 @@ import {
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from '@storybook/test';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
|
||||
const meta: Meta<typeof JsonTree> = {
|
||||
@ -14,7 +13,7 @@ const meta: Meta<typeof JsonTree> = {
|
||||
component: JsonTree,
|
||||
args: {},
|
||||
argTypes: {},
|
||||
decorators: [ComponentDecorator, I18nFrontDecorator],
|
||||
decorators: [I18nFrontDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@ -51,10 +50,47 @@ export const ArraySimple: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ArrayEmpty: Story = {
|
||||
args: {
|
||||
value: [],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const emptyState = await canvas.findByText('Empty Array');
|
||||
|
||||
expect(emptyState).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ArrayNested: Story = {
|
||||
args: {
|
||||
value: [1, 2, ['a', 'b', 'c'], 3],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedArrayElements = await canvas.findByText('[3]');
|
||||
|
||||
expect(nestedArrayElements).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ArrayNestedEmpty: Story = {
|
||||
args: {
|
||||
value: [1, 2, [], 3],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedArrayElements = await canvas.findByText('[0]');
|
||||
|
||||
expect(nestedArrayElements).toBeVisible();
|
||||
|
||||
const emptyState = await canvas.findByText('Empty Array');
|
||||
|
||||
expect(emptyState).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ArrayWithObjects: Story = {
|
||||
@ -70,6 +106,13 @@ export const ArrayWithObjects: Story = {
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedObjectItemsCounts = await canvas.findAllByText('{2}');
|
||||
|
||||
expect(nestedObjectItemsCounts).toHaveLength(2);
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectSimple: Story = {
|
||||
@ -81,6 +124,19 @@ export const ObjectSimple: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectEmpty: Story = {
|
||||
args: {
|
||||
value: {},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const emptyState = await canvas.findByText('Empty Object');
|
||||
|
||||
expect(emptyState).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectNested: Story = {
|
||||
args: {
|
||||
value: {
|
||||
@ -94,6 +150,32 @@ export const ObjectNested: Story = {
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedObjectItemsCounts = await canvas.findAllByText('{2}');
|
||||
|
||||
expect(nestedObjectItemsCounts).toHaveLength(2);
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectNestedEmpty: Story = {
|
||||
args: {
|
||||
value: {
|
||||
person: {},
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedObjectItemsCount = await canvas.findByText('{0}');
|
||||
|
||||
expect(nestedObjectItemsCount).toBeVisible();
|
||||
|
||||
const emptyState = await canvas.findByText('Empty Object');
|
||||
|
||||
expect(emptyState).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectWithArray: Story = {
|
||||
@ -187,3 +269,123 @@ export const ExpandingElementExpandsAllItsDescendants: Story = {
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const ReallyDeepNestedObject: Story = {
|
||||
args: {
|
||||
value: {
|
||||
a: {
|
||||
b: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: {
|
||||
g: {
|
||||
h: {
|
||||
i: {
|
||||
j: {
|
||||
k: {
|
||||
l: {
|
||||
m: {
|
||||
n: {
|
||||
o: {
|
||||
p: {
|
||||
q: {
|
||||
r: {
|
||||
s: {
|
||||
t: {
|
||||
u: {
|
||||
v: {
|
||||
w: {
|
||||
x: {
|
||||
y: {
|
||||
z: {
|
||||
end: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bis: {
|
||||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: {
|
||||
g: {
|
||||
h: {
|
||||
i: {
|
||||
j: {
|
||||
k: {
|
||||
l: {
|
||||
m: {
|
||||
n: {
|
||||
o: {
|
||||
p: {
|
||||
q: {
|
||||
r: {
|
||||
s: {
|
||||
t: {
|
||||
u: {
|
||||
v: {
|
||||
w: {
|
||||
x: {
|
||||
y: {
|
||||
z: {
|
||||
end: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LongText: Story = {
|
||||
args: {
|
||||
value: {
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum iaculis est tincidunt, sagittis neque vitae, sodales purus.':
|
||||
'Ut lobortis ultricies purus, sit amet porta eros. Suspendisse efficitur quam vitae diam imperdiet feugiat. Etiam vel bibendum elit.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { JsonNestedNode } from '@/workflow/components/json-visualizer/components/JsonNestedNode';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconBrackets } from 'twenty-ui';
|
||||
import { JsonArray } from 'type-fest';
|
||||
|
||||
@ -11,6 +12,8 @@ export const JsonArrayNode = ({
|
||||
value: JsonArray;
|
||||
depth: number;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<JsonNestedNode
|
||||
elements={[...value.entries()].map(([key, value]) => ({
|
||||
@ -18,9 +21,11 @@ export const JsonArrayNode = ({
|
||||
label: String(key),
|
||||
value,
|
||||
}))}
|
||||
renderElementsCount={(count) => `[${count}]`}
|
||||
label={label}
|
||||
Icon={IconBrackets}
|
||||
depth={depth}
|
||||
emptyElementsText={t`Empty Array`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -20,15 +20,27 @@ const StyledLabelContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledElementsCount = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export const JsonNestedNode = ({
|
||||
label,
|
||||
Icon,
|
||||
elements,
|
||||
renderElementsCount,
|
||||
emptyElementsText,
|
||||
depth,
|
||||
}: {
|
||||
label?: string;
|
||||
Icon: IconComponent;
|
||||
elements: Array<{ id: string | number; label: string; value: JsonValue }>;
|
||||
renderElementsCount?: (count: number) => string;
|
||||
emptyElementsText: string;
|
||||
depth: number;
|
||||
}) => {
|
||||
const hideRoot = !isDefined(label);
|
||||
@ -37,9 +49,13 @@ export const JsonNestedNode = ({
|
||||
|
||||
const renderedChildren = (
|
||||
<JsonList depth={depth}>
|
||||
{elements.map(({ id, label, value }) => (
|
||||
<JsonNode key={id} label={label} value={value} depth={depth + 1} />
|
||||
))}
|
||||
{elements.length === 0 ? (
|
||||
<StyledEmptyState>{emptyElementsText}</StyledEmptyState>
|
||||
) : (
|
||||
elements.map(({ id, label, value }) => (
|
||||
<JsonNode key={id} label={label} value={value} depth={depth + 1} />
|
||||
))
|
||||
)}
|
||||
</JsonList>
|
||||
);
|
||||
|
||||
@ -57,6 +73,12 @@ export const JsonNestedNode = ({
|
||||
<JsonArrow isOpen={isOpen} onClick={handleArrowClick} />
|
||||
|
||||
<JsonNodeLabel label={label} Icon={Icon} />
|
||||
|
||||
{renderElementsCount && (
|
||||
<StyledElementsCount>
|
||||
{renderElementsCount(elements.length)}
|
||||
</StyledElementsCount>
|
||||
)}
|
||||
</StyledLabelContainer>
|
||||
|
||||
{isOpen && renderedChildren}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { JsonNestedNode } from '@/workflow/components/json-visualizer/components/JsonNestedNode';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconCube } from 'twenty-ui';
|
||||
import { JsonObject } from 'type-fest';
|
||||
|
||||
@ -11,6 +12,8 @@ export const JsonObjectNode = ({
|
||||
value: JsonObject;
|
||||
depth: number;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return (
|
||||
<JsonNestedNode
|
||||
elements={Object.entries(value).map(([key, value]) => ({
|
||||
@ -18,9 +21,11 @@ export const JsonObjectNode = ({
|
||||
label: key,
|
||||
value,
|
||||
}))}
|
||||
renderElementsCount={(count) => `{${count}}`}
|
||||
label={label}
|
||||
Icon={IconCube}
|
||||
depth={depth}
|
||||
emptyElementsText={t`Empty Object`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ const StyledListItem = styled.li`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export { StyledListItem as JsonListItem };
|
||||
|
||||
Reference in New Issue
Block a user