mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-22 14:09:46 +03:00
Merge 8e07c1d1d3
into 398746ba75
This commit is contained in:
commit
2449c05220
5
.changeset/modern-masks-fetch.md
Normal file
5
.changeset/modern-masks-fetch.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'react-json-tree': minor
|
||||
---
|
||||
|
||||
Add expand/collapse all feature
|
|
@ -137,6 +137,21 @@ Their full signatures are:
|
|||
- `labelRenderer: function(keyPath, nodeType, expanded, expandable)`
|
||||
- `valueRenderer: function(valueAsString, value, ...keyPath)`
|
||||
|
||||
#### Customize "Expand All/Collapse All" Buttons
|
||||
|
||||
Passing the `expandCollapseAll` props will activate in the top right corner of the JSONTree component the `expand all/collapse all` buttons. You can pass a JSON to customize the expand all/collapse all icons. The default icons are from [FontAwesome](https://fontawesome.com/).
|
||||
|
||||
```jsx
|
||||
<JSONTree
|
||||
expandCollapseAll={{
|
||||
defaultValue: 'expand',
|
||||
expandIcon: /* your custom expand icon */,
|
||||
collapseIcon: /* your custom collapse icon */,
|
||||
defaultIcon: /* your custom restore to default icon */,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
#### More Options
|
||||
|
||||
- `shouldExpandNodeInitially: function(keyPath, data, level)` - determines if node should be expanded when it first renders (root is expanded by default)
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"react": "^18.2.0",
|
||||
"react-base16-styling": "^0.9.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-json-tree": "^0.18.0"
|
||||
"react-json-tree": "link:.."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
|
|
|
@ -46,6 +46,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"react-base16-styling": "^0.9.1"
|
||||
},
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import JSONArrow from './JSONArrow';
|
||||
import getCollectionEntries from './getCollectionEntries';
|
||||
import JSONNode from './JSONNode';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import ItemRange from './ItemRange';
|
||||
import JSONArrow from './JSONArrow';
|
||||
import JSONNode from './JSONNode';
|
||||
import { useExpandCollapseAllContext } from './expandCollapseContext';
|
||||
import getCollectionEntries from './getCollectionEntries';
|
||||
import type { CircularCache, CommonInternalProps } from './types';
|
||||
|
||||
/**
|
||||
|
@ -112,23 +113,61 @@ export default function JSONNestedNode(props: Props) {
|
|||
shouldExpandNodeInitially,
|
||||
styling,
|
||||
} = props;
|
||||
const { expandAllState, setExpandAllState, setEnableDefaultButton } =
|
||||
useExpandCollapseAllContext();
|
||||
|
||||
const [expanded, setExpanded] = useState<boolean>(
|
||||
const [defaultExpanded] = useState<boolean>(
|
||||
// calculate individual node expansion if necessary
|
||||
isCircular ? false : shouldExpandNodeInitially(keyPath, data, level)
|
||||
isCircular
|
||||
? false
|
||||
: (function getDefault() {
|
||||
switch (expandAllState) {
|
||||
case 'expand':
|
||||
return true;
|
||||
case 'collapse':
|
||||
return false;
|
||||
default:
|
||||
return shouldExpandNodeInitially(keyPath, data, level);
|
||||
}
|
||||
})()
|
||||
);
|
||||
|
||||
const [, setTriggerReRender] = useState<boolean>(defaultExpanded);
|
||||
|
||||
/**
|
||||
* Used the useRef to handle expanded because calling a setState in a recursive implementation
|
||||
* could lead to a "Maximum update depth exceeded" error */
|
||||
const expandedRef = useRef<boolean>(defaultExpanded);
|
||||
|
||||
switch (expandAllState) {
|
||||
case 'expand':
|
||||
expandedRef.current = isCircular ? false : true;
|
||||
break;
|
||||
case 'collapse':
|
||||
expandedRef.current = false;
|
||||
break;
|
||||
case 'default':
|
||||
expandedRef.current = shouldExpandNodeInitially(keyPath, data, level);
|
||||
break;
|
||||
default: //Do nothing;
|
||||
}
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (expandable) setExpanded(!expanded);
|
||||
}, [expandable, expanded]);
|
||||
if (expandable) {
|
||||
expandedRef.current = !expandedRef.current;
|
||||
setTriggerReRender((e) => !e);
|
||||
setEnableDefaultButton(true);
|
||||
setExpandAllState(undefined);
|
||||
}
|
||||
}, [expandable, setEnableDefaultButton, setExpandAllState]);
|
||||
|
||||
const renderedChildren =
|
||||
expanded || (hideRoot && level === 0)
|
||||
expandedRef.current || (hideRoot && level === 0)
|
||||
? renderChildNodes({ ...props, circularCache, level: level + 1 })
|
||||
: null;
|
||||
|
||||
const itemType = (
|
||||
<span {...styling('nestedNodeItemType', expanded)}>
|
||||
<span {...styling('nestedNodeItemType', expandedRef.current)}>
|
||||
{nodeTypeIndicator}
|
||||
</span>
|
||||
);
|
||||
|
@ -137,9 +176,15 @@ export default function JSONNestedNode(props: Props) {
|
|||
data,
|
||||
itemType,
|
||||
createItemString(data, collectionLimit),
|
||||
keyPath
|
||||
keyPath,
|
||||
expandedRef.current,
|
||||
);
|
||||
const stylingArgs = [keyPath, nodeType, expanded, expandable] as const;
|
||||
const stylingArgs = [
|
||||
keyPath,
|
||||
nodeType,
|
||||
expandedRef.current,
|
||||
expandable,
|
||||
] as const;
|
||||
|
||||
return hideRoot ? (
|
||||
<li {...styling('rootNode', ...stylingArgs)}>
|
||||
|
@ -153,7 +198,7 @@ export default function JSONNestedNode(props: Props) {
|
|||
<JSONArrow
|
||||
styling={styling}
|
||||
nodeType={nodeType}
|
||||
expanded={expanded}
|
||||
expanded={expandedRef.current}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -46,6 +46,7 @@ const getDefaultThemeStyling = (theme: Base16Theme): StylingConfig => {
|
|||
|
||||
return {
|
||||
tree: {
|
||||
position: 'relative',
|
||||
border: 0,
|
||||
padding: 0,
|
||||
marginTop: '0.5em',
|
||||
|
@ -58,6 +59,19 @@ const getDefaultThemeStyling = (theme: Base16Theme): StylingConfig => {
|
|||
backgroundColor: colors.BACKGROUND_COLOR,
|
||||
},
|
||||
|
||||
expandCollapseAll: {
|
||||
color: colors.TEXT_COLOR,
|
||||
backgroundColor: colors.BACKGROUND_COLOR,
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
top: '1rem',
|
||||
right: '1rem',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
value: ({ style }, nodeType, keyPath) => ({
|
||||
style: {
|
||||
...style,
|
||||
|
|
129
packages/react-json-tree/src/expandCollapseButtons.tsx
Normal file
129
packages/react-json-tree/src/expandCollapseButtons.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
import {
|
||||
faArrowDown,
|
||||
faArrowRight,
|
||||
faUndo,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { ExpandCollapseAll } from '.';
|
||||
import { useExpandCollapseAllContext } from './expandCollapseContext';
|
||||
import { StylingFunction } from 'react-base16-styling';
|
||||
|
||||
interface Props {
|
||||
expandCollapseAll: ExpandCollapseAll;
|
||||
styling: StylingFunction;
|
||||
}
|
||||
|
||||
interface ExpandButtonProps {
|
||||
expandableDefaultValue?: 'expand' | 'collapse';
|
||||
expandIcon?: ReactNode;
|
||||
}
|
||||
|
||||
interface CollapseButtonProps {
|
||||
expandableDefaultValue?: 'expand' | 'collapse';
|
||||
collapseIcon?: ReactNode;
|
||||
}
|
||||
|
||||
interface DefaultButtonProps {
|
||||
defaultIcon?: ReactNode;
|
||||
}
|
||||
|
||||
function ExpandCollapseButtons({ expandCollapseAll, styling }: Props) {
|
||||
const { enableDefaultButton } = useExpandCollapseAllContext();
|
||||
|
||||
const expandableDefaultValue = expandCollapseAll?.defaultValue || 'expand';
|
||||
|
||||
return (
|
||||
<div {...styling('expandCollapseAll')}>
|
||||
{enableDefaultButton && (
|
||||
<DefaultButton defaultIcon={expandCollapseAll?.defaultIcon} />
|
||||
)}
|
||||
|
||||
<ExpandButton
|
||||
expandableDefaultValue={expandableDefaultValue}
|
||||
expandIcon={expandCollapseAll?.expandIcon}
|
||||
/>
|
||||
|
||||
<CollapseButton
|
||||
expandableDefaultValue={expandCollapseAll?.defaultValue}
|
||||
collapseIcon={expandCollapseAll?.collapseIcon}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ExpandButton({
|
||||
expandableDefaultValue,
|
||||
expandIcon,
|
||||
}: ExpandButtonProps) {
|
||||
const { expandAllState, setExpandAllState, setEnableDefaultButton } =
|
||||
useExpandCollapseAllContext();
|
||||
|
||||
const onExpand = () => {
|
||||
setExpandAllState('expand');
|
||||
setEnableDefaultButton(true);
|
||||
};
|
||||
|
||||
const isDefault = !expandAllState || expandAllState === 'default';
|
||||
|
||||
if (
|
||||
expandAllState === 'collapse' ||
|
||||
(isDefault && expandableDefaultValue === 'expand')
|
||||
) {
|
||||
return (
|
||||
<div role="presentation" onClick={onExpand}>
|
||||
{expandIcon || <FontAwesomeIcon icon={faArrowRight} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function CollapseButton({
|
||||
expandableDefaultValue,
|
||||
collapseIcon,
|
||||
}: CollapseButtonProps) {
|
||||
const { expandAllState, setExpandAllState, setEnableDefaultButton } =
|
||||
useExpandCollapseAllContext();
|
||||
|
||||
const onCollapse = () => {
|
||||
setExpandAllState('collapse');
|
||||
setEnableDefaultButton(true);
|
||||
};
|
||||
|
||||
const isDefault = !expandAllState || expandAllState === 'default';
|
||||
|
||||
if (
|
||||
expandAllState === 'expand' ||
|
||||
(isDefault && expandableDefaultValue === 'collapse')
|
||||
) {
|
||||
return (
|
||||
<div role="presentation" onClick={onCollapse}>
|
||||
{collapseIcon || <FontAwesomeIcon icon={faArrowDown} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function DefaultButton({ defaultIcon }: DefaultButtonProps) {
|
||||
const { setExpandAllState, setEnableDefaultButton } =
|
||||
useExpandCollapseAllContext();
|
||||
|
||||
const onDefaultCollapse = () => {
|
||||
setExpandAllState('default');
|
||||
setEnableDefaultButton(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div role="presentation" onClick={onDefaultCollapse}>
|
||||
{defaultIcon || <FontAwesomeIcon icon={faUndo} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export default ExpandCollapseButtons;
|
61
packages/react-json-tree/src/expandCollapseContext.tsx
Normal file
61
packages/react-json-tree/src/expandCollapseContext.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React, {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ExpandCollapseAll } from '.';
|
||||
import ExpandCollapseButtons from './expandCollapseButtons';
|
||||
import { StylingFunction } from 'react-base16-styling';
|
||||
|
||||
interface Context {
|
||||
enableDefaultButton: boolean;
|
||||
setEnableDefaultButton: any;
|
||||
expandAllState?: 'expand' | 'collapse' | 'default';
|
||||
setExpandAllState: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
expandCollapseAll?: ExpandCollapseAll;
|
||||
styling: StylingFunction;
|
||||
}
|
||||
|
||||
const ExpandCollapseAllContext = createContext<Context>({} as Context);
|
||||
|
||||
function ExpandCollapseAllContextProvider({
|
||||
expandCollapseAll,
|
||||
children,
|
||||
styling,
|
||||
}: Props) {
|
||||
const [enableDefaultButton, setEnableDefaultButton] = useState(false);
|
||||
const [expandAllState, setExpandAllState] = useState();
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
enableDefaultButton,
|
||||
setEnableDefaultButton,
|
||||
expandAllState,
|
||||
setExpandAllState,
|
||||
}),
|
||||
[enableDefaultButton, expandAllState]
|
||||
);
|
||||
|
||||
return (
|
||||
<ExpandCollapseAllContext.Provider value={value}>
|
||||
{children}
|
||||
{expandCollapseAll && (
|
||||
<ExpandCollapseButtons
|
||||
expandCollapseAll={expandCollapseAll}
|
||||
styling={styling}
|
||||
/>
|
||||
)}
|
||||
</ExpandCollapseAllContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useExpandCollapseAllContext = () =>
|
||||
useContext(ExpandCollapseAllContext);
|
||||
|
||||
export default ExpandCollapseAllContextProvider;
|
|
@ -3,11 +3,13 @@
|
|||
// Dave Vedder <veddermatic@gmail.com> http://www.eskimospy.com/
|
||||
// port by Daniele Zannotti http://www.github.com/dzannotti <dzannotti@me.com>
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { ReactNode, useMemo } from 'react';
|
||||
import JSONNode from './JSONNode';
|
||||
import createStylingFromTheme from './createStylingFromTheme';
|
||||
import { invertTheme } from 'react-base16-styling';
|
||||
import type { StylingValue, Theme } from 'react-base16-styling';
|
||||
import ExpandCollapseAllButtonsContext from './expandCollapseContext';
|
||||
|
||||
import type {
|
||||
CommonExternalProps,
|
||||
GetItemString,
|
||||
|
@ -20,6 +22,14 @@ interface Props extends Partial<CommonExternalProps> {
|
|||
data: unknown;
|
||||
theme?: Theme;
|
||||
invertTheme?: boolean;
|
||||
expandCollapseAll?: ExpandCollapseAll;
|
||||
}
|
||||
|
||||
interface ExpandCollapseAll {
|
||||
defaultValue?: 'expand' | 'collapse';
|
||||
expandIcon?: ReactNode;
|
||||
collapseIcon?: ReactNode;
|
||||
defaultIcon?: ReactNode;
|
||||
}
|
||||
|
||||
const identity = (value: any) => value;
|
||||
|
@ -41,6 +51,7 @@ export function JSONTree({
|
|||
labelRenderer = defaultLabelRenderer,
|
||||
valueRenderer = identity,
|
||||
shouldExpandNodeInitially = expandRootNode,
|
||||
expandCollapseAll,
|
||||
hideRoot = false,
|
||||
getItemString = defaultItemString,
|
||||
postprocessValue = identity,
|
||||
|
@ -56,20 +67,25 @@ export function JSONTree({
|
|||
|
||||
return (
|
||||
<ul {...styling('tree')}>
|
||||
<JSONNode
|
||||
keyPath={hideRoot ? [] : keyPath}
|
||||
value={postprocessValue(value)}
|
||||
isCustomNode={isCustomNode}
|
||||
<ExpandCollapseAllButtonsContext
|
||||
expandCollapseAll={expandCollapseAll}
|
||||
styling={styling}
|
||||
labelRenderer={labelRenderer}
|
||||
valueRenderer={valueRenderer}
|
||||
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
||||
hideRoot={hideRoot}
|
||||
getItemString={getItemString}
|
||||
postprocessValue={postprocessValue}
|
||||
collectionLimit={collectionLimit}
|
||||
sortObjectKeys={sortObjectKeys}
|
||||
/>
|
||||
>
|
||||
<JSONNode
|
||||
keyPath={hideRoot ? [] : keyPath}
|
||||
value={postprocessValue(value)}
|
||||
isCustomNode={isCustomNode}
|
||||
styling={styling}
|
||||
labelRenderer={labelRenderer}
|
||||
valueRenderer={valueRenderer}
|
||||
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
||||
hideRoot={hideRoot}
|
||||
getItemString={getItemString}
|
||||
postprocessValue={postprocessValue}
|
||||
collectionLimit={collectionLimit}
|
||||
sortObjectKeys={sortObjectKeys}
|
||||
/>
|
||||
</ExpandCollapseAllButtonsContext>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
@ -87,4 +103,4 @@ export type {
|
|||
Styling,
|
||||
CommonExternalProps,
|
||||
} from './types';
|
||||
export type { StylingValue };
|
||||
export type { ExpandCollapseAll, StylingValue };
|
||||
|
|
|
@ -10,7 +10,8 @@ export type GetItemString = (
|
|||
data: unknown,
|
||||
itemType: React.ReactNode,
|
||||
itemString: string,
|
||||
keyPath: KeyPath
|
||||
keyPath: KeyPath,
|
||||
isExpanded: boolean,
|
||||
) => React.ReactNode;
|
||||
|
||||
export type LabelRenderer = (
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { createRenderer } from 'react-test-renderer/shallow';
|
||||
|
||||
import { JSONTree } from '../src/index';
|
||||
import JSONNode from '../src/JSONNode';
|
||||
import ExpandCollapseAllContext from '../src/expandCollapseContext';
|
||||
|
||||
const BASIC_DATA = { a: 1, b: 'c' };
|
||||
|
||||
|
@ -17,6 +17,6 @@ describe('JSONTree', () => {
|
|||
const result = render(<JSONTree data={BASIC_DATA} />);
|
||||
|
||||
expect(result.type).toBe('ul');
|
||||
expect(result.props.children.type.name).toBe(JSONNode.name);
|
||||
expect(result.props.children.type.name).toBe(ExpandCollapseAllContext.name);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user