From d4f318e7419f195bf9c8a8e7f327c1f428850d65 Mon Sep 17 00:00:00 2001 From: n1c0de Date: Mon, 26 May 2025 13:52:13 +0200 Subject: [PATCH 1/2] feat: add custom buttons to rows according to value type --- dev-server/src/index.js | 200 +++++++++++++++++++++++++- index.d.ts | 16 +++ src/js/components/ArrayGroup.js | 3 +- src/js/components/CustomButton.js | 83 +++++++++++ src/js/components/DataTypes/Object.js | 4 +- src/js/components/JsonViewer.js | 2 +- src/js/components/VariableEditor.js | 19 ++- src/js/components/VariableMeta.js | 14 +- src/js/components/icons.js | 51 +++++++ src/js/themes/getStyle.js | 10 ++ 10 files changed, 394 insertions(+), 8 deletions(-) create mode 100644 src/js/components/CustomButton.js diff --git a/dev-server/src/index.js b/dev-server/src/index.js index 229ae0e..2d5b1fa 100644 --- a/dev-server/src/index.js +++ b/dev-server/src/index.js @@ -137,7 +137,7 @@ ReactDom.render(
- {/*demo array support*/} + {/* demo array support */} + + {/* Custom buttons according to the value type */} + { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + integer: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: (element) => element.src === 27 + ? + : , + viewBox: (element) => element.src === 27 + ? '0 0 15 15' + : '0 0 40 40', + title: (element) => element.src === 27 + ? 'Special title' + : 'Example title', + className: (element) => element.src === 27 + ? 'special-class' + : 'class-example' + }, + float: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + bigNumber: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + date: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + string: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: (element) => element.variableName === 'string-key-test' + ? + : , + viewBox: (element) => element.variableName === 'string-key-test' + ?'0 0 1792 1792' + :'0 0 40 40', + title: (element) => element.variableName === 'string-key-test' + ? 'Special title' + : 'Title example', + className: (element) => element.variableName === 'string-key-test' + ? 'special-class' + : 'class-example' + }, + regexp: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + array: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + empty_array: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + object: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + empty_object: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + function: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + undefined: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + null: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + nan: { + clickCallback: (element) => { console.log(JSON.stringify(element, null, 4)) }, + path: , + viewBox: '0 0 40 40', + title: 'A title example', + className: 'class-example' + }, + }} + onEdit={e => { + console.log('edit callback', e) + if (e.new_value == 'error') { + return false + } + }} + onDelete={e => { + console.log('delete callback', e) + }} + onAdd={e => { + console.log('add callback', e) + if (e.new_value == 'error') { + return false + } + }} + onSelect={e => { + console.log('select callback', e) + console.log(e.namespace) + }} + displayObjectSize={true} + name={'custom-buttons'} + enableClipboard={copy => { + console.log('you copied to clipboard!', copy) + }} + shouldCollapse={({ src, namespace, type }) => { + if (type === 'array' && src.indexOf('test') > -1) { + return true + } else if (namespace.indexOf('moment') > -1) { + return true + } + return false + }} + defaultValue='' + /> , document.getElementById('app-container') ) @@ -320,3 +486,35 @@ function getExampleArray () { function getExampleWithStringEscapeSequences () { return { '\\\n\t\r\f\\n': '\\\n\t\r\f\\n' } } + +function getExampleJson5 () { + return { + string: 'this is a test string', + 'string-key-test': 'this is another test string', + integer: 42, + 'integer-key-test': 27, + empty_array: [], + empty_object: {}, + array: [1, 2, 3, 'test'], + float: -2.757, + undefined_var: undefined, + parent: { + sibling1: true, + sibling2: false, + sibling3: null, + sibling4: NaN, + isString: value => { + if (typeof value === 'string') { + return 'string' + } else { + return 'other' + } + } + }, + string_number: '1234', + date: new Date(), + moment: Moment(), + regexp: /[0-9]/gi, + bigNumber: new BigNumber('0.0060254656709730629123'), + } +} diff --git a/index.d.ts b/index.d.ts index 3d49b19..5b8e7ba 100644 --- a/index.d.ts +++ b/index.d.ts @@ -165,6 +165,12 @@ export interface ReactJsonViewProps { * Default: true */ escapeStrings?: boolean + /** + * Adds custom buttons according to the value type. + * + * Default: null + */ + customButtons?: TypeCustomButtons } export interface OnCopyProps { @@ -308,5 +314,15 @@ export type ThemeKeys = | 'tube' | 'twilight' +export type TypeCustomButtons = { + [valueType: string]: { + clickCallback: (element: { variableName: string; src: string; namespace: Array; name: string; }) => void + path: React.ReactElement> | ((element: { variableName: string; src: string; namespace: Array; name: string; }) => React.ReactElement>) + viewBox?: string | ((element: { variableName: string; src: string; namespace: Array; name: string; }) => string) + title?: string | ((element: { variableName: string; src: string; namespace: Array; name: string; }) => string) + className?: string | ((element: { variableName: string; src: string; namespace: Array; name: string; }) => string) + } +} + declare const ReactJson: React.ComponentType export default ReactJson diff --git a/src/js/components/ArrayGroup.js b/src/js/components/ArrayGroup.js index 55d6122..c6e66e7 100644 --- a/src/js/components/ArrayGroup.js +++ b/src/js/components/ArrayGroup.js @@ -50,6 +50,7 @@ export default class extends React.PureComponent { jsvRoot, namespace, parent_type, + customButtons, ...rest } = this.props @@ -74,7 +75,7 @@ export default class extends React.PureComponent { - + {[...Array(groups)].map((_, i) => (
{ + const { clickCallback, src, namespace, variableName } = this.props + + clickCallback({ + variableName: variableName, + src: src, + namespace: namespace, + name: namespace[namespace.length - 1] + }) + } + + getClippyIcon = () => { + const { theme } = this.props + + return ( + + ) + } + + render () { + const { src, theme, hidden, rowHovered, namespace, variableName, title, className } = this.props; + const style = Theme(theme, 'custom-button').style + let display = 'inline' + + if (hidden) { + display = 'none' + } + + return ( + + + {this.getClippyIcon()} + + + ) + } +} diff --git a/src/js/components/DataTypes/Object.js b/src/js/components/DataTypes/Object.js index a7a0605..c50573d 100644 --- a/src/js/components/DataTypes/Object.js +++ b/src/js/components/DataTypes/Object.js @@ -130,9 +130,9 @@ class RjvObject extends React.PureComponent { } getObjectMetaData = src => { - const { rjvId, theme } = this.props + const { rjvId, theme, customButtons } = this.props const { size, hovered } = this.state - return + return } getBraceStart (object_type, expanded) { diff --git a/src/js/components/JsonViewer.js b/src/js/components/JsonViewer.js index 41f4b88..b18abcc 100644 --- a/src/js/components/JsonViewer.js +++ b/src/js/components/JsonViewer.js @@ -25,7 +25,7 @@ export default class extends React.PureComponent { return (
- +
) diff --git a/src/js/components/VariableEditor.js b/src/js/components/VariableEditor.js index d0bcb32..1caf114 100644 --- a/src/js/components/VariableEditor.js +++ b/src/js/components/VariableEditor.js @@ -6,6 +6,7 @@ import dispatcher from './../helpers/dispatcher' import parseInput from './../helpers/parseInput' import stringifyVariable from './../helpers/stringifyVariable' import CopyToClipboard from './CopyToClipboard' +import CustomButton from './CustomButton' // data type components import { @@ -57,9 +58,11 @@ class VariableEditor extends React.PureComponent { onSelect, displayArrayKey, quotesOnKeys, - keyModifier + keyModifier, + customButtons } = this.props const { editMode } = this.state + return (
) : null} + {customButtons[variable.type] + ? ( +
@@ -331,7 +346,7 @@ class VariableEditor extends React.PureComponent { if (BigNumber && parsedInput.type === 'bigNumber') { new_value = new BigNumber(new_value) } - } + } this.setState({ editMode: false }) diff --git a/src/js/components/VariableMeta.js b/src/js/components/VariableMeta.js index 1da8d9d..337fc76 100644 --- a/src/js/components/VariableMeta.js +++ b/src/js/components/VariableMeta.js @@ -2,6 +2,7 @@ import React from 'react' import dispatcher from './../helpers/dispatcher' import CopyToClipboard from './CopyToClipboard' +import CustomButton from './CustomButton' import { toType } from './../helpers/util' // icons @@ -108,7 +109,9 @@ export default class extends React.PureComponent { enableClipboard, src, namespace, - rowHovered + rowHovered, + name, + customButtons } = this.props return (
) : null} + {customButtons[toType(src)] + ? ( + + ) + : null} {/* copy add/remove icons */} {onAdd !== false ? this.getAddAttribute(rowHovered) : null} {onDelete !== false ? this.getRemoveObject(rowHovered) : null} diff --git a/src/js/components/icons.js b/src/js/components/icons.js index 0f5e41f..566622c 100644 --- a/src/js/components/icons.js +++ b/src/js/components/icons.js @@ -266,6 +266,57 @@ export class CheckCircle extends React.PureComponent { } } +export class CustomIcon extends React.PureComponent { + handleViewBox = () => { + const { viewBox, src, namespace, variableName } = this.props + + return viewBox({ + variableName: variableName, + src: src, + namespace: namespace, + name: namespace[namespace.length - 1] + }) + } + + render () { + const { props } = this + const { style, path, viewBox, src, namespace, variableName } = props + + return ( + + + + {typeof path === 'function' + ? path({ + variableName: variableName, + src: src, + namespace: namespace, + name: namespace[namespace.length - 1] + }) + : path + } + + + + ) + } +} + function getIconStyle (style) { if (!style) { style = {} diff --git a/src/js/themes/getStyle.js b/src/js/themes/getStyle.js index a8b290d..f0273ca 100644 --- a/src/js/themes/getStyle.js +++ b/src/js/themes/getStyle.js @@ -234,6 +234,16 @@ const getDefaultThemeStyling = theme => { color: colors.copyToClipboardCheck, marginLeft: constants.clipboardCheckMarginLeft }, + 'custom-button': { + cursor: 'pointer' + }, + 'custom-icon': { + color: 'currentColor', + fontSize: constants.iconFontSize, + marginRight: constants.iconMarginRight, + verticalAlign: 'top', + transition: 'color 150ms ease-in-out' + }, 'array-group-meta-data': { display: 'inline-block', padding: constants.arrayGroupMetaPadding From dde8679df94f9b009417e85288dbbeb85e7956b0 Mon Sep 17 00:00:00 2001 From: n1c0de Date: Tue, 27 May 2025 11:32:42 +0200 Subject: [PATCH 2/2] test: add a custom button test --- test/tests/js/components/CustomButton-test.js | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 test/tests/js/components/CustomButton-test.js diff --git a/test/tests/js/components/CustomButton-test.js b/test/tests/js/components/CustomButton-test.js new file mode 100644 index 0000000..ac3c2ce --- /dev/null +++ b/test/tests/js/components/CustomButton-test.js @@ -0,0 +1,55 @@ +import React from 'react' +import { shallow } from 'enzyme' +import { expect } from 'chai' +import sinon from 'sinon' + +import CustomButton from './../../../../src/js/components/CustomButton' + +const pathJSX = ( + +); + +describe('', function () { + it('CustomButton component should exist', function () { + const logSpy = sinon.spy(console, 'log') + const wrapper = shallow( + console.log('It works!')} + path={pathJSX} + viewBox='0 0 40 40' + title='A title example' + className='class-example' + hidden={false} + /> + ) + + expect(wrapper.find('span')).to.have.length(1) + try { + wrapper.find('.class-example').simulate('click') + expect(logSpy.calledWith('It works!')).to.be.true + } finally { + logSpy.restore() + } + expect( + wrapper.find('svg').containsMatchingElement(pathJSX) + ).to.be.true + expect(wrapper.find('svg').prop('viewBox')).to.equal('0 0 40 40') + expect(wrapper.find('.class-example').prop('title')).to.equal('A title example') + expect(wrapper.find('.class-example').prop('className')).to.equal('class-example') + }) + + it('CustomButton component should be hidden', function () { + const wrapper = shallow( + console.log('It works!')} + path={pathJSX} + viewBox='0 0 40 40' + title='A title example' + className='class-example' + hidden + /> + ) + + expect(wrapper.find('span').prop('style')).to.have.property('display', 'none') + }) +})