From 105b002e43e77d3f3d83d0297207896cb8dd6ceb Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Mon, 5 Dec 2022 21:10:43 +0300 Subject: [PATCH] feat: collapse all empty iterables and disable expanding them (#123) --- docs/pages/full/index.tsx | 4 + src/components/DataKeyPair.tsx | 14 ++- src/components/DataTypes/Object.tsx | 69 +++++------ src/hooks/useInspect.ts | 3 +- src/utils/index.ts | 17 +++ tests/index.test.tsx | 174 ++++++++++++++++++++++++++++ 6 files changed, 235 insertions(+), 46 deletions(-) diff --git a/docs/pages/full/index.tsx b/docs/pages/full/index.tsx index daf17a73..eccfc074 100644 --- a/docs/pages/full/index.tsx +++ b/docs/pages/full/index.tsx @@ -67,12 +67,15 @@ const example = { string: 'this is a string', integer: 42, array: [19, 19, 810, 'test', NaN], + emptyArray: [], nestedArray: [ [1, 2], [3, 4] ], map, + emptyMap: new Map(), set, + emptySet: new Set(), float: 114.514, undefined, superLongString, @@ -81,6 +84,7 @@ const example = { 'second-child': false, 'last-child': null }, + emptyObject: {}, function: aPlusB, constFunction: aPlusBConst, anonymousFunction: function (a: number, b: number) { diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx index 34606415..5e18fcab 100644 --- a/src/components/DataKeyPair.tsx +++ b/src/components/DataKeyPair.tsx @@ -16,6 +16,7 @@ import { useInspect } from '../hooks/useInspect' import { useJsonViewerStore } from '../stores/JsonViewerStore' import { useTypeComponents } from '../stores/typeRegistry' import type { DataItemProps } from '../type' +import { getValueSize } from '../utils' import { DataBox } from './mui/DataBox' export type DataKeyPairProps = { @@ -62,10 +63,8 @@ export const DataKeyPair: React.FC = (props) => { const [editing, setEditing] = useState(false) const onChange = useJsonViewerStore(store => store.onChange) const keyColor = useTextColor() - const numberKeyColor = useJsonViewerStore( - store => store.colorspace.base0C) - const { Component, PreComponent, PostComponent, Editor } = useTypeComponents( - value, path) + const numberKeyColor = useJsonViewerStore(store => store.colorspace.base0C) + const { Component, PreComponent, PostComponent, Editor } = useTypeComponents(value, path) const quotesOnKeys = useJsonViewerStore(store => store.quotesOnKeys) const rootName = useJsonViewerStore(store => store.rootName) const isRoot = root === value @@ -176,7 +175,8 @@ export const DataKeyPair: React.FC = (props) => { value ]) - const expandable = !!(PreComponent && PostComponent) + const isEmptyValue = useMemo(() => getValueSize(value) === 0, [value]) + const expandable = !isEmptyValue && !!(PreComponent && PostComponent) const KeyRenderer = useJsonViewerStore(store => store.keyRenderer) const downstreamProps: DataItemProps = useMemo(() => ({ path, @@ -206,7 +206,9 @@ export const DataKeyPair: React.FC = (props) => { if (event.isDefaultPrevented()) { return } - setInspect(state => !state) + if (!isEmptyValue) { + setInspect(state => !state) + } }, [setInspect]) } > diff --git a/src/components/DataTypes/Object.tsx b/src/components/DataTypes/Object.tsx index 5a88a0c9..3d929969 100644 --- a/src/components/DataTypes/Object.tsx +++ b/src/components/DataTypes/Object.tsx @@ -6,6 +6,7 @@ import { useTextColor } from '../../hooks/useColor' import { useIsCycleReference } from '../../hooks/useIsCycleReference' import { useJsonViewerStore } from '../../stores/JsonViewerStore' import type { DataItemProps } from '../../type' +import { getValueSize } from '../../utils' import { DataKeyPair } from '../DataKeyPair' import { CircularArrowsIcon } from '../icons/CircularArrowsIcon' import { DataBox } from '../mui/DataBox' @@ -16,15 +17,11 @@ const objectRb = '}' const arrayRb = ']' function inspectMetadata (value: object) { - let length + const length = getValueSize(value) + let name = '' - if (Array.isArray(value)) { - length = value.length - } else if (value instanceof Map || value instanceof Set) { + if (value instanceof Map || value instanceof Set) { name = value[Symbol.toStringTag] - length = value.size - } else { - length = Object.keys(value).length } if (Object.prototype.hasOwnProperty.call(value, Symbol.toStringTag)) { name = (value as any)[Symbol.toStringTag] @@ -36,9 +33,8 @@ export const PreObjectType: React.FC> = (props) => { const metadataColor = useJsonViewerStore(store => store.colorspace.base04) const textColor = useTextColor() const isArray = useMemo(() => Array.isArray(props.value), [props.value]) - const sizeOfValue = useMemo( - () => props.inspect ? inspectMetadata(props.value) : '', - [props.inspect, props.value] + const isEmptyValue = useMemo(() => getValueSize(props.value) === 0, [props.value]) + const sizeOfValue = useMemo(() => inspectMetadata(props.value), [props.inspect, props.value] ) const displayObjectSize = useJsonViewerStore(store => store.displayObjectSize) const isTrap = useIsCycleReference(props.path, props.value) @@ -50,20 +46,18 @@ export const PreObjectType: React.FC> = (props) => { }} > {isArray ? arrayLb : objectLb} - {displayObjectSize - ? ( - - {sizeOfValue} - - ) - : null} + {displayObjectSize && props.inspect && !isEmptyValue && ( + + {sizeOfValue} + + )} {isTrap && !props.inspect ? ( @@ -85,14 +79,13 @@ export const PostObjectType: React.FC> = (props) => { const metadataColor = useJsonViewerStore(store => store.colorspace.base04) const isArray = useMemo(() => Array.isArray(props.value), [props.value]) const displayObjectSize = useJsonViewerStore(store => store.displayObjectSize) - const sizeOfValue = useMemo( - () => !props.inspect ? inspectMetadata(props.value) : '', - [props.inspect, props.value] - ) + const isEmptyValue = useMemo(() => getValueSize(props.value) === 0, [props.value]) + const sizeOfValue = useMemo(() => inspectMetadata(props.value), [props.inspect, props.value]) + return ( {isArray ? arrayRb : objectRb} - {displayObjectSize + {displayObjectSize && (isEmptyValue || !props.inspect) ? ( { export const ObjectType: React.FC> = (props) => { const keyColor = useTextColor() const borderColor = useJsonViewerStore(store => store.colorspace.base02) - const groupArraysAfterLength = useJsonViewerStore( - store => store.groupArraysAfterLength) + const groupArraysAfterLength = useJsonViewerStore(store => store.groupArraysAfterLength) const isTrap = useIsCycleReference(props.path, props.value) - const [displayLength, setDisplayLength] = useState( - useJsonViewerStore(store => store.maxDisplayLength) - ) + const [displayLength, setDisplayLength] = useState(useJsonViewerStore(store => store.maxDisplayLength)) const objectSortKeys = useJsonViewerStore(store => store.objectSortKeys) const elements = useMemo(() => { if (!props.inspect) { @@ -201,10 +191,9 @@ export const ObjectType: React.FC> = (props) => { // object let entries: [key: string, value: unknown][] = Object.entries(value) if (objectSortKeys) { - entries = entries.sort(([a], [b]) => objectSortKeys === true - ? a.localeCompare(b) - : objectSortKeys(a, b) - ) + entries = objectSortKeys === true + ? entries.sort(([a], [b]) => a.localeCompare(b)) + : entries.sort(([a], [b]) => objectSortKeys(a, b)) } const elements = entries.slice(0, displayLength).map(([key, value]) => { const path = [...props.path, key] @@ -243,6 +232,10 @@ export const ObjectType: React.FC> = (props) => { const marginLeft = props.inspect ? 0.6 : 0 const width = useJsonViewerStore(store => store.indentWidth) const indentWidth = props.inspect ? width - marginLeft : width + const isEmptyValue = useMemo(() => getValueSize(props.value) === 0, [props.value]) + if (isEmptyValue) { + return null + } return ( store.getInspectCache) const setInspectCache = useJsonViewerStore(store => store.setInspectCache) - const defaultInspectDepth = useJsonViewerStore( - store => store.defaultInspectDepth) + const defaultInspectDepth = useJsonViewerStore(store => store.defaultInspectDepth) useEffect(() => { const inspect = getInspectCache(path, nestedIndex) if (inspect !== undefined) { diff --git a/src/utils/index.ts b/src/utils/index.ts index 9fc33c50..da876c8e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -123,3 +123,20 @@ export const isCycleReference = ( } return false } + +export function getValueSize (value: any): number { + if (value === null || undefined) { + return 0 + } else if (Array.isArray(value)) { + return value.length + } else if (value instanceof Map || value instanceof Set) { + return value.size + } else if (value instanceof Date) { + return 1 + } else if (typeof value === 'object') { + return Object.keys(value).length + } else if (typeof value === 'string') { + return value.length + } + return 1 +} diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 35832b5b..dbd74db0 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -445,6 +445,180 @@ describe('Expand function by click on dots', () => { }) }) +describe('See empty iterables', () => { + it('Array', () => { + const { container } = render( + + ) + + let elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + elements = container.getElementsByClassName('data-object-start') + expect(elements.length).eq(1) + elements = container.getElementsByClassName('data-object-end') + expect(elements.length).eq(1) + }) + it('Object', () => { + const { container } = render( + + ) + + let elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + elements = container.getElementsByClassName('data-object-start') + expect(elements.length).eq(1) + elements = container.getElementsByClassName('data-object-end') + expect(elements.length).eq(1) + }) + it('Map', () => { + const { container } = render( + + ) + + let elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + elements = container.getElementsByClassName('data-object-start') + expect(elements.length).eq(1) + elements = container.getElementsByClassName('data-object-end') + expect(elements.length).eq(1) + }) + it('Set', () => { + const { container } = render( + + ) + + let elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + elements = container.getElementsByClassName('data-object-start') + expect(elements.length).eq(1) + elements = container.getElementsByClassName('data-object-end') + expect(elements.length).eq(1) + }) +}) + +describe('Click on empty iterables', () => { + it('Array', () => { + const Component = () => + const { container, rerender } = render() + + // Click on start brace + let elements = container.getElementsByClassName('data-object-start') + expect(elements.length).eq(1) + fireEvent.click(elements[0]) + + rerender() + elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + + // Click on end brace + elements = container.getElementsByClassName('data-object-end') + expect(elements.length).eq(1) + fireEvent.click(elements[0]) + + rerender() + elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + }) + it('Object', () => { + const Component = () => + const { container, rerender } = render() + + // Click on start brace + let elements = container.getElementsByClassName('data-object-start') + expect(elements.length).eq(1) + fireEvent.click(elements[0]) + + rerender() + elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + + // Click on end brace + elements = container.getElementsByClassName('data-object-end') + expect(elements.length).eq(1) + fireEvent.click(elements[0]) + + rerender() + elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + }) + it('Map', () => { + const Component = () => + const { container, rerender } = render() + + // Click on start brace + let elements = container.getElementsByClassName('data-object-start') + expect(elements.length).eq(1) + fireEvent.click(elements[0]) + + rerender() + elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + + // Click on end brace + elements = container.getElementsByClassName('data-object-end') + expect(elements.length).eq(1) + fireEvent.click(elements[0]) + + rerender() + elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + }) + it('Set', () => { + const Component = () => + const { container, rerender } = render() + + // Click on start brace + let elements = container.getElementsByClassName('data-object-start') + expect(elements.length).eq(1) + fireEvent.click(elements[0]) + + rerender() + elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + + // Click on end brace + elements = container.getElementsByClassName('data-object-end') + expect(elements.length).eq(1) + fireEvent.click(elements[0]) + + rerender() + elements = container.getElementsByClassName('data-object-body') + expect(elements.length).eq(0) + }) +}) + describe('Show three dots after string collapsing', () => { it('render', () => { const Component = () =>