Rename twenty-ui to twenty-ui-deprecated and twenty-new-ui to twenty-ui to prepare package release (#21315)
## Description Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name **`twenty-ui`** (v0.1.0, publishable) and renames the old package to **`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports → `twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates twenty-front's `Toggle` to the new package (first consumer) as a drop-in. ## Next steps - Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` + `.yarnrc.yml`), then tag `ui/v0.1.0` to publish. - Continue migrating components from `twenty-ui-deprecated` → `twenty-ui`.
This commit is contained in:
@@ -1,615 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { JsonTree } from '@ui/json-visualizer/components/JsonTree';
|
||||
import { isTwoFirstDepths } from '@ui/json-visualizer/utils/isTwoFirstDepths';
|
||||
import {
|
||||
expect,
|
||||
fn,
|
||||
userEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from 'storybook/test';
|
||||
|
||||
const meta: Meta<typeof JsonTree> = {
|
||||
title: 'UI/JsonVisualizer/JsonTree',
|
||||
component: JsonTree,
|
||||
args: {
|
||||
shouldExpandNodeInitially: () => true,
|
||||
emptyArrayLabel: 'Empty Array',
|
||||
emptyObjectLabel: 'Empty Object',
|
||||
emptyStringLabel: '[empty string]',
|
||||
arrowButtonCollapsedLabel: 'Expand',
|
||||
arrowButtonExpandedLabel: 'Collapse',
|
||||
},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof JsonTree>;
|
||||
|
||||
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 = {
|
||||
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 = {
|
||||
args: {
|
||||
value: [
|
||||
{
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
},
|
||||
{
|
||||
name: 'John Dowl',
|
||||
age: 42,
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nestedObjectItemsCounts = await canvas.findAllByText('{2}');
|
||||
|
||||
expect(nestedObjectItemsCounts).toHaveLength(2);
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectSimple: Story = {
|
||||
args: {
|
||||
value: {
|
||||
name: 'John Doe',
|
||||
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 = {
|
||||
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: {
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'New York',
|
||||
},
|
||||
},
|
||||
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 = {
|
||||
args: {
|
||||
value: {
|
||||
users: [
|
||||
{ id: 1, name: 'John' },
|
||||
{ id: 2, name: 'Jane' },
|
||||
],
|
||||
settings: {
|
||||
theme: 'dark',
|
||||
notifications: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
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 = {
|
||||
args: {
|
||||
value: {
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
age: 12,
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const toggleButton = await canvas.findByRole('button', {
|
||||
name: 'Collapse',
|
||||
});
|
||||
|
||||
const ageElement = canvas.getByText('age');
|
||||
|
||||
await Promise.all([
|
||||
waitForElementToBeRemoved(ageElement),
|
||||
|
||||
userEvent.click(toggleButton),
|
||||
]);
|
||||
|
||||
expect(toggleButton).toHaveTextContent('Expand');
|
||||
},
|
||||
};
|
||||
|
||||
export const ExpandingElementExpandsAllItsDescendants: Story = {
|
||||
args: {
|
||||
value: {
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'New York',
|
||||
country: {
|
||||
name: 'USA',
|
||||
code: 'US',
|
||||
},
|
||||
},
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
{
|
||||
const allCollapseButtons = await canvas.findAllByRole('button', {
|
||||
name: 'Collapse',
|
||||
});
|
||||
|
||||
expect(allCollapseButtons).toHaveLength(3);
|
||||
|
||||
for (const collapseButton of allCollapseButtons.reverse()) {
|
||||
await userEvent.click(collapseButton);
|
||||
}
|
||||
}
|
||||
|
||||
const rootExpandButton = await canvas.findByRole('button', {
|
||||
name: 'Expand',
|
||||
});
|
||||
|
||||
await userEvent.click(rootExpandButton);
|
||||
|
||||
{
|
||||
const allCollapseButtons = await canvas.findAllByRole('button', {
|
||||
name: 'Collapse',
|
||||
});
|
||||
|
||||
expect(allCollapseButtons).toHaveLength(3);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const ExpandTwoFirstDepths: Story = {
|
||||
args: {
|
||||
value: {
|
||||
person: {
|
||||
name: 'John Doe',
|
||||
address: {
|
||||
street: '123 Main St',
|
||||
city: 'New York',
|
||||
country: {
|
||||
name: 'USA',
|
||||
code: 'US',
|
||||
},
|
||||
},
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
shouldExpandNodeInitially: isTwoFirstDepths,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nameElement = await canvas.findByText('name');
|
||||
expect(nameElement).toBeVisible();
|
||||
|
||||
const addressElement = await canvas.findByText('address');
|
||||
expect(addressElement).toBeVisible();
|
||||
|
||||
const streetElement = canvas.queryByText('street');
|
||||
expect(streetElement).not.toBeInTheDocument();
|
||||
|
||||
const countrCodeElement = canvas.queryByText('code');
|
||||
expect(countrCodeElement).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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 = {
|
||||
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.',
|
||||
},
|
||||
},
|
||||
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 = {
|
||||
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 PartialBlueHighlighting: Story = {
|
||||
args: {
|
||||
value: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
address: {
|
||||
city: 'Paris',
|
||||
},
|
||||
},
|
||||
getNodeHighlighting: (keyPath: string) =>
|
||||
keyPath === 'address' ? 'partial-blue' : undefined,
|
||||
},
|
||||
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,
|
||||
address: {
|
||||
city: 'Paris',
|
||||
},
|
||||
},
|
||||
getNodeHighlighting: () => 'red',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const ageElement = await canvas.findByText('age');
|
||||
expect(ageElement).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const CopyJsonNodeValue: Story = {
|
||||
args: {
|
||||
value: {
|
||||
name: 'John Doe',
|
||||
age: 30,
|
||||
},
|
||||
onNodeValueClick: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nameValue = await canvas.findByText('John Doe');
|
||||
|
||||
await userEvent.click(nameValue);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onNodeValueClick).toHaveBeenCalledWith('John Doe');
|
||||
});
|
||||
|
||||
const ageValue = await canvas.findByText('30');
|
||||
|
||||
await userEvent.click(ageValue);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onNodeValueClick).toHaveBeenCalledWith('30');
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { IconBrackets } from '@ui/display';
|
||||
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { type JsonArray } from 'type-fest';
|
||||
|
||||
export const JsonArrayNode = ({
|
||||
label,
|
||||
value,
|
||||
depth,
|
||||
keyPath,
|
||||
highlighting,
|
||||
}: {
|
||||
label?: string;
|
||||
value: JsonArray;
|
||||
depth: number;
|
||||
keyPath: string;
|
||||
highlighting: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { emptyArrayLabel } = useJsonTreeContextOrThrow();
|
||||
|
||||
return (
|
||||
<JsonNestedNode
|
||||
elements={[...value.entries()].map(([key, value]) => ({
|
||||
id: key,
|
||||
label: String(key),
|
||||
value,
|
||||
}))}
|
||||
renderElementsCount={(count) => `[${count}]`}
|
||||
label={label}
|
||||
Icon={IconBrackets}
|
||||
depth={depth}
|
||||
emptyElementsText={emptyArrayLabel}
|
||||
keyPath={keyPath}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,157 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type IconComponent } from '@ui/display';
|
||||
import { JsonArrow } from '@ui/json-visualizer/components/internal/JsonArrow';
|
||||
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
|
||||
import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue';
|
||||
import { JsonNode } from '@ui/json-visualizer/components/JsonNode';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
const StyledContainer = styled.li`
|
||||
display: grid;
|
||||
list-style-type: none;
|
||||
`;
|
||||
|
||||
const StyledLabelContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledElementsCount = styled.span<{
|
||||
variant?: 'red';
|
||||
}>`
|
||||
color: ${({ variant }) =>
|
||||
variant === 'red'
|
||||
? themeCssVariables.font.color.danger
|
||||
: themeCssVariables.font.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledJsonListBase = styled.ul<{
|
||||
depth: number;
|
||||
}>`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
row-gap: ${themeCssVariables.spacing[2]};
|
||||
padding-left: ${({ depth }) =>
|
||||
depth > 0 ? themeCssVariables.spacing[8] : '0'};
|
||||
|
||||
> :first-of-type {
|
||||
margin-top: ${({ depth }) =>
|
||||
depth > 0 ? themeCssVariables.spacing[2] : '0'};
|
||||
}
|
||||
`;
|
||||
|
||||
export const JsonNestedNode = ({
|
||||
label,
|
||||
Icon,
|
||||
elements,
|
||||
renderElementsCount,
|
||||
emptyElementsText,
|
||||
depth,
|
||||
keyPath,
|
||||
highlighting,
|
||||
}: {
|
||||
label?: string;
|
||||
Icon: IconComponent;
|
||||
elements: Array<{ id: string | number; label: string; value: JsonValue }>;
|
||||
renderElementsCount?: (count: number) => string;
|
||||
emptyElementsText: string;
|
||||
depth: number;
|
||||
keyPath: string;
|
||||
highlighting?: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { shouldExpandNodeInitially } = useJsonTreeContextOrThrow();
|
||||
|
||||
const hideRoot = !isDefined(label);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(
|
||||
shouldExpandNodeInitially({ keyPath, depth }),
|
||||
);
|
||||
|
||||
const renderedChildren = (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0, overflow: 'clip' }}
|
||||
animate={{ height: 'auto', opacity: 1, overflow: 'clip' }}
|
||||
exit={{ height: 0, opacity: 0, overflow: 'clip' }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<StyledJsonListBase depth={depth}>
|
||||
{elements.length === 0 ? (
|
||||
<JsonNodeValue valueAsString={emptyElementsText} />
|
||||
) : (
|
||||
elements.map(({ id, label, value }) => {
|
||||
const nextKeyPath = isNonEmptyString(keyPath)
|
||||
? `${keyPath}.${id}`
|
||||
: String(id);
|
||||
|
||||
return (
|
||||
<JsonNode
|
||||
key={id}
|
||||
label={label}
|
||||
value={value}
|
||||
depth={depth + 1}
|
||||
keyPath={nextKeyPath}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</StyledJsonListBase>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
const handleArrowClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
if (hideRoot) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<AnimatePresence initial={false}>{renderedChildren}</AnimatePresence>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLabelContainer>
|
||||
<JsonArrow
|
||||
isOpen={isOpen}
|
||||
onClick={handleArrowClick}
|
||||
variant={
|
||||
highlighting === 'partial-blue'
|
||||
? 'blue'
|
||||
: highlighting === 'red'
|
||||
? highlighting
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<JsonNodeLabel
|
||||
label={label}
|
||||
Icon={Icon}
|
||||
highlighting={highlighting === 'red' ? highlighting : undefined}
|
||||
/>
|
||||
|
||||
{renderElementsCount && (
|
||||
<StyledElementsCount
|
||||
variant={highlighting === 'red' ? 'red' : undefined}
|
||||
>
|
||||
{renderElementsCount(elements.length)}
|
||||
</StyledElementsCount>
|
||||
)}
|
||||
</StyledLabelContainer>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{isOpen && renderedChildren}
|
||||
</AnimatePresence>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import {
|
||||
isBoolean,
|
||||
isNonEmptyString,
|
||||
isNumber,
|
||||
isString,
|
||||
} from '@sniptt/guards';
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconCircleOff,
|
||||
IconNumber9,
|
||||
IconTypography,
|
||||
} from '@ui/display';
|
||||
import { JsonArrayNode } from '@ui/json-visualizer/components/JsonArrayNode';
|
||||
import { JsonObjectNode } from '@ui/json-visualizer/components/JsonObjectNode';
|
||||
import { JsonValueNode } from '@ui/json-visualizer/components/JsonValueNode';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { isArray } from '@ui/json-visualizer/utils/isArray';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
export const JsonNode = ({
|
||||
label,
|
||||
value,
|
||||
depth,
|
||||
keyPath,
|
||||
}: {
|
||||
label?: string;
|
||||
value: JsonValue;
|
||||
depth: number;
|
||||
keyPath: string;
|
||||
}) => {
|
||||
const { getNodeHighlighting, emptyStringLabel } = useJsonTreeContextOrThrow();
|
||||
|
||||
const highlighting = getNodeHighlighting?.(keyPath);
|
||||
|
||||
if (!isDefined(value)) {
|
||||
return (
|
||||
<JsonValueNode
|
||||
label={label}
|
||||
valueAsString="null"
|
||||
Icon={IconCircleOff}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isString(value)) {
|
||||
return (
|
||||
<JsonValueNode
|
||||
label={label}
|
||||
valueAsString={isNonEmptyString(value) ? value : emptyStringLabel}
|
||||
Icon={IconTypography}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNumber(value)) {
|
||||
return (
|
||||
<JsonValueNode
|
||||
label={label}
|
||||
valueAsString={String(value)}
|
||||
Icon={IconNumber9}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isBoolean(value)) {
|
||||
return (
|
||||
<JsonValueNode
|
||||
label={label}
|
||||
valueAsString={String(value)}
|
||||
Icon={IconCheckbox}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isArray(value)) {
|
||||
return (
|
||||
<JsonArrayNode
|
||||
label={label}
|
||||
value={value}
|
||||
depth={depth}
|
||||
keyPath={keyPath}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<JsonObjectNode
|
||||
label={label}
|
||||
value={value}
|
||||
depth={depth}
|
||||
keyPath={keyPath}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { IconCube } from '@ui/display';
|
||||
import { JsonNestedNode } from '@ui/json-visualizer/components/JsonNestedNode';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { type JsonObject } from 'type-fest';
|
||||
|
||||
export const JsonObjectNode = ({
|
||||
label,
|
||||
value,
|
||||
depth,
|
||||
keyPath,
|
||||
highlighting,
|
||||
}: {
|
||||
label?: string;
|
||||
value: JsonObject;
|
||||
depth: number;
|
||||
keyPath: string;
|
||||
highlighting: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { emptyObjectLabel } = useJsonTreeContextOrThrow();
|
||||
|
||||
return (
|
||||
<JsonNestedNode
|
||||
elements={Object.entries(value).map(([key, value]) => ({
|
||||
id: key,
|
||||
label: key,
|
||||
value,
|
||||
}))}
|
||||
renderElementsCount={(count) => `{${count}}`}
|
||||
label={label}
|
||||
Icon={IconCube}
|
||||
depth={depth}
|
||||
emptyElementsText={emptyObjectLabel}
|
||||
keyPath={keyPath}
|
||||
highlighting={highlighting}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
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 { type ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||
import { type GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
export const JsonTree = ({
|
||||
value,
|
||||
getNodeHighlighting,
|
||||
shouldExpandNodeInitially,
|
||||
emptyArrayLabel,
|
||||
emptyObjectLabel,
|
||||
emptyStringLabel,
|
||||
arrowButtonCollapsedLabel,
|
||||
arrowButtonExpandedLabel,
|
||||
onNodeValueClick,
|
||||
}: {
|
||||
value: JsonValue;
|
||||
getNodeHighlighting?: GetJsonNodeHighlighting;
|
||||
shouldExpandNodeInitially: (
|
||||
params: ShouldExpandNodeInitiallyProps,
|
||||
) => boolean;
|
||||
emptyArrayLabel: string;
|
||||
emptyObjectLabel: string;
|
||||
emptyStringLabel: string;
|
||||
arrowButtonCollapsedLabel: string;
|
||||
arrowButtonExpandedLabel: string;
|
||||
onNodeValueClick?: (valueAsString: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<JsonTreeContextProvider
|
||||
value={{
|
||||
getNodeHighlighting,
|
||||
shouldExpandNodeInitially,
|
||||
emptyArrayLabel,
|
||||
emptyObjectLabel,
|
||||
emptyStringLabel,
|
||||
arrowButtonCollapsedLabel,
|
||||
arrowButtonExpandedLabel,
|
||||
onNodeValueClick,
|
||||
}}
|
||||
>
|
||||
<JsonList depth={0}>
|
||||
<JsonNode value={value} depth={0} keyPath="" />
|
||||
</JsonList>
|
||||
</JsonTreeContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
JsonTreeContext,
|
||||
type JsonTreeContextType,
|
||||
} from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||
|
||||
export const JsonTreeContextProvider = ({
|
||||
value,
|
||||
children,
|
||||
}: {
|
||||
value: JsonTreeContextType;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<JsonTreeContext.Provider value={value}>
|
||||
{children}
|
||||
</JsonTreeContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type IconComponent } from '@ui/display';
|
||||
import { JsonNodeLabel } from '@ui/json-visualizer/components/internal/JsonNodeLabel';
|
||||
import { JsonNodeValue } from '@ui/json-visualizer/components/internal/JsonNodeValue';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledListItem = styled.li`
|
||||
align-items: center;
|
||||
column-gap: ${themeCssVariables.spacing[2]};
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type JsonValueNodeProps = {
|
||||
valueAsString: string;
|
||||
highlighting: JsonNodeHighlighting | undefined;
|
||||
} & (
|
||||
| {
|
||||
label: string;
|
||||
Icon: IconComponent;
|
||||
}
|
||||
| {
|
||||
label?: never;
|
||||
Icon?: unknown;
|
||||
}
|
||||
);
|
||||
|
||||
export const JsonValueNode = (props: JsonValueNodeProps) => {
|
||||
return (
|
||||
<StyledListItem>
|
||||
{props.label && (
|
||||
<JsonNodeLabel
|
||||
label={props.label}
|
||||
Icon={props.Icon}
|
||||
highlighting={props.highlighting}
|
||||
/>
|
||||
)}
|
||||
|
||||
<JsonNodeValue
|
||||
valueAsString={props.valueAsString}
|
||||
highlighting={props.highlighting}
|
||||
/>
|
||||
</StyledListItem>
|
||||
);
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { VisibilityHidden } from '@ui/accessibility';
|
||||
import { IconChevronDown } from '@ui/display';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
|
||||
import { useContext } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const StyledButton = styled.button<{
|
||||
variant?: 'blue' | 'red';
|
||||
}>`
|
||||
align-items: center;
|
||||
background-color: ${({ variant }) =>
|
||||
variant === 'red'
|
||||
? themeCssVariables.background.danger
|
||||
: themeCssVariables.background.transparent.lighter};
|
||||
border-color: ${({ variant }) =>
|
||||
variant === 'red'
|
||||
? themeCssVariables.border.color.danger
|
||||
: themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-inline: ${themeCssVariables.spacing[1]};
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const JsonArrow = ({
|
||||
isOpen,
|
||||
onClick,
|
||||
variant,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClick: () => void;
|
||||
variant?: 'blue' | 'red';
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { arrowButtonCollapsedLabel, arrowButtonExpandedLabel } =
|
||||
useJsonTreeContextOrThrow();
|
||||
|
||||
const iconColor =
|
||||
variant === 'blue'
|
||||
? themeCssVariables.color.blue
|
||||
: variant === 'red'
|
||||
? themeCssVariables.font.color.danger
|
||||
: themeCssVariables.font.color.secondary;
|
||||
|
||||
return (
|
||||
<StyledButton variant={variant} onClick={onClick}>
|
||||
<VisibilityHidden>
|
||||
{isOpen ? arrowButtonExpandedLabel : arrowButtonCollapsedLabel}
|
||||
</VisibilityHidden>
|
||||
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ rotate: isOpen ? 0 : -90 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<IconChevronDown size={theme.icon.size.md} color={iconColor} />
|
||||
</motion.div>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledList = styled.ul<{ depth: number }>`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
display: grid;
|
||||
row-gap: ${themeCssVariables.spacing[2]};
|
||||
|
||||
${({ depth }) =>
|
||||
depth > 0
|
||||
? `
|
||||
padding-left: ${themeCssVariables.spacing[8]};
|
||||
|
||||
> :first-of-type {
|
||||
margin-top: ${themeCssVariables.spacing[2]};
|
||||
}
|
||||
`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
export { StyledList as JsonList };
|
||||
@@ -1,10 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledListItem = styled.li`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export { StyledListItem as JsonListItem };
|
||||
@@ -1,74 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type IconComponent } from '@ui/display';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const StyledLabelContainer = styled.span<{
|
||||
highlighting?: JsonNodeHighlighting;
|
||||
}>`
|
||||
background-color: ${({ highlighting }) =>
|
||||
highlighting === 'blue'
|
||||
? themeCssVariables.color.blue3
|
||||
: highlighting === 'red'
|
||||
? themeCssVariables.background.danger
|
||||
: themeCssVariables.background.transparent.lighter};
|
||||
border-color: ${({ highlighting }) =>
|
||||
highlighting === 'blue'
|
||||
? themeCssVariables.color.blue5
|
||||
: highlighting === 'red'
|
||||
? themeCssVariables.border.color.danger
|
||||
: themeCssVariables.border.color.medium};
|
||||
color: ${({ highlighting }) =>
|
||||
highlighting === 'blue'
|
||||
? themeCssVariables.color.blue
|
||||
: highlighting === 'red'
|
||||
? themeCssVariables.font.color.danger
|
||||
: themeCssVariables.font.color.primary};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
column-gap: ${themeCssVariables.spacing[2]};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
white-space: nowrap;
|
||||
padding-inline: ${themeCssVariables.spacing[2]};
|
||||
|
||||
> span {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
line-height: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const JsonNodeLabel = ({
|
||||
label,
|
||||
Icon,
|
||||
highlighting,
|
||||
}: {
|
||||
label: string;
|
||||
Icon: IconComponent;
|
||||
highlighting?: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<StyledLabelContainer highlighting={highlighting}>
|
||||
<Icon
|
||||
size={theme.icon.size.md}
|
||||
color={
|
||||
highlighting === 'blue'
|
||||
? theme.color.blue
|
||||
: highlighting === 'red'
|
||||
? theme.font.color.danger
|
||||
: theme.font.color.tertiary
|
||||
}
|
||||
/>
|
||||
|
||||
<span>{label}</span>
|
||||
</StyledLabelContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useJsonTreeContextOrThrow } from '@ui/json-visualizer/hooks/useJsonTreeContextOrThrow';
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledText = styled.span<{
|
||||
highlighting: JsonNodeHighlighting | undefined;
|
||||
}>`
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
color: ${({ highlighting }) =>
|
||||
highlighting === 'blue'
|
||||
? themeCssVariables.color.blue8
|
||||
: highlighting === 'red'
|
||||
? themeCssVariables.color.red8
|
||||
: themeCssVariables.font.color.tertiary};
|
||||
display: inline-flex;
|
||||
height: 24px;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
export const JsonNodeValue = ({
|
||||
valueAsString,
|
||||
highlighting,
|
||||
}: {
|
||||
valueAsString: string;
|
||||
highlighting?: JsonNodeHighlighting | undefined;
|
||||
}) => {
|
||||
const { onNodeValueClick } = useJsonTreeContextOrThrow();
|
||||
|
||||
const handleClick = () => {
|
||||
onNodeValueClick?.(valueAsString);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledText highlighting={highlighting} onClick={handleClick}>
|
||||
{valueAsString}
|
||||
</StyledText>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { type GetJsonNodeHighlighting } from '@ui/json-visualizer/types/GetJsonNodeHighlighting';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type ShouldExpandNodeInitiallyProps = { keyPath: string; depth: number };
|
||||
|
||||
export type JsonTreeContextType = {
|
||||
getNodeHighlighting?: GetJsonNodeHighlighting;
|
||||
shouldExpandNodeInitially: (
|
||||
params: ShouldExpandNodeInitiallyProps,
|
||||
) => boolean;
|
||||
emptyStringLabel: string;
|
||||
emptyArrayLabel: string;
|
||||
emptyObjectLabel: string;
|
||||
arrowButtonCollapsedLabel: string;
|
||||
arrowButtonExpandedLabel: string;
|
||||
onNodeValueClick?: (valueAsString: string) => void;
|
||||
};
|
||||
|
||||
export const JsonTreeContext = createContext<JsonTreeContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
@@ -1,15 +0,0 @@
|
||||
import { JsonTreeContext } from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useJsonTreeContextOrThrow = () => {
|
||||
const value = useContext(JsonTreeContext);
|
||||
|
||||
if (!isDefined(value)) {
|
||||
throw new Error(
|
||||
'useJsonTreeContextOrThrow must be used within a JsonTreeContextProvider',
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
@@ -7,20 +7,4 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export { JsonArrayNode } from './components/JsonArrayNode';
|
||||
export { JsonNestedNode } from './components/JsonNestedNode';
|
||||
export { JsonNode } from './components/JsonNode';
|
||||
export { JsonObjectNode } from './components/JsonObjectNode';
|
||||
export { JsonTree } from './components/JsonTree';
|
||||
export { JsonTreeContextProvider } from './components/JsonTreeContextProvider';
|
||||
export { JsonValueNode } from './components/JsonValueNode';
|
||||
export type {
|
||||
ShouldExpandNodeInitiallyProps,
|
||||
JsonTreeContextType,
|
||||
} from './contexts/JsonTreeContext';
|
||||
export { JsonTreeContext } from './contexts/JsonTreeContext';
|
||||
export { useJsonTreeContextOrThrow } from './hooks/useJsonTreeContextOrThrow';
|
||||
export type { GetJsonNodeHighlighting } from './types/GetJsonNodeHighlighting';
|
||||
export type { JsonNodeHighlighting } from './types/JsonNodeHighlighting';
|
||||
export { isArray } from './utils/isArray';
|
||||
export { isTwoFirstDepths } from './utils/isTwoFirstDepths';
|
||||
export {};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { type JsonNodeHighlighting } from '@ui/json-visualizer/types/JsonNodeHighlighting';
|
||||
|
||||
export type GetJsonNodeHighlighting = (
|
||||
keyPath: string,
|
||||
) => JsonNodeHighlighting | undefined;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { type ThemeColor } from '@ui/theme';
|
||||
|
||||
export type JsonNodeHighlighting =
|
||||
| Extract<ThemeColor, 'blue' | 'red'>
|
||||
| 'partial-blue';
|
||||
@@ -1,5 +0,0 @@
|
||||
import { isArray as _isArray } from '@sniptt/guards';
|
||||
|
||||
export const isArray = (
|
||||
value: unknown,
|
||||
): value is unknown[] | readonly unknown[] => _isArray(value);
|
||||
@@ -1,4 +0,0 @@
|
||||
import { type ShouldExpandNodeInitiallyProps } from '@ui/json-visualizer/contexts/JsonTreeContext';
|
||||
|
||||
export const isTwoFirstDepths = ({ depth }: ShouldExpandNodeInitiallyProps) =>
|
||||
depth <= 1;
|
||||
Reference in New Issue
Block a user