add json editor for json data type (fix #504) (#1181)

This commit is contained in:
Anupam Dagar 2019-05-24 20:40:39 +05:30 committed by Rikin Kachhia
parent 2cdb1a1508
commit 71557cdecc
8 changed files with 259 additions and 88 deletions

View File

@ -0,0 +1,137 @@
import React, { useState } from 'react';
import AceEditor from 'react-ace';
const styles = require('./JsonInput.scss');
const NORMALKEY = 'normal';
const JSONKEY = 'json';
const parseJSONData = (data, editorType) => {
try {
const dataObject = typeof data === 'object' ? data : JSON.parse(data);
return JSON.stringify(dataObject, null, editorType === JSONKEY ? 4 : 0);
} catch (e) {
return data;
}
};
const createInitialState = data => {
const initialState = {
editorType: NORMALKEY,
data: parseJSONData(data, NORMALKEY),
};
return initialState;
};
const JsonInput = props => {
const { standardProps, placeholderProp } = props;
const { defaultValue, onChange } = standardProps;
const allProps = { ...standardProps };
delete allProps.defaultValue;
const [state, updateState] = useState(createInitialState(defaultValue));
const { editorType, data } = state;
const updateData = (newData, currentState) => {
return {
...currentState,
data: newData,
};
};
const toggleEditorType = currentState => {
const nextEditorType =
currentState.editorType === JSONKEY ? NORMALKEY : JSONKEY;
return {
...currentState,
data: parseJSONData(currentState.data, nextEditorType),
editorType: nextEditorType,
};
};
const handleKeyUpEvent = e => {
if ((e.ctrlKey || event.metaKey) && e.which === 32) {
updateState(toggleEditorType);
}
};
const handleEditorExec = () => {
updateState(toggleEditorType);
};
const handleInputChangeAndPropagate = e => {
const val = e.target.value;
updateState(currentState => updateData(val, currentState));
if (onChange) {
onChange(e);
}
};
const handleTextAreaChangeAndPropagate = (value, e) => {
const val = value;
updateState(currentState => updateData(val, currentState));
if (onChange) {
onChange(e, value);
}
};
const getJsonEditor = () => {
return (
<AceEditor
key="ace_json_editor"
{...allProps}
mode="json"
theme="github"
name="jsontoggler"
minLines={10}
maxLines={100}
width="100%"
value={data}
showPrintMargin={false}
onChange={handleTextAreaChangeAndPropagate}
showGutter={false}
focus
commands={[
{
name: 'toggleEditor',
bindKey: { win: 'Ctrl-Space', mac: 'Command-Space' },
exec: handleEditorExec,
},
]}
/>
);
};
const getNormalEditor = () => {
return (
<input
key="input_json_editor"
{...allProps}
placeholder={`${placeholderProp} (Ctrl + Space to toggle)`}
value={data}
onChange={handleInputChangeAndPropagate}
onKeyUp={handleKeyUpEvent}
className={allProps.className + ' ' + styles.jsonNormalInput}
/>
);
};
const editor = editorType === JSONKEY ? getJsonEditor() : getNormalEditor();
return (
<span className="json_input_editor">
<label>{editor}</label>
<i
key="icon_json_editor"
className={
'fa ' +
styles.jsonToggleButton +
(editorType === JSONKEY ? ' fa-compress' : ' fa-expand')
}
onClick={() => updateState(toggleEditorType)}
/>
</span>
);
};
export default JsonInput;

View File

@ -0,0 +1,14 @@
.jsonNormalInput {
padding-right: 30px;
}
.jsonToggleButton {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
opacity: 0.3;
&:hover {
opacity: 1.0;
}
}

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { getMeParentNode } from '../../../utils/domFunctions';
import { getParentNodeByAttribute } from '../../../utils/domFunctions';
import Button from '../Button/Button';
const styles = require('./Dropdown.scss');
@ -65,7 +65,7 @@ const Dropdown = ({ keyPrefix, testId, children, options, position }) => {
/*
* Update the state only if the element clicked on is not the data dropdown component
* */
const dataElement = getMeParentNode(e.target, 'data-element');
const dataElement = getParentNodeByAttribute(e.target, 'data-element');
if (d) {
/* If the element has parent whose `nodeId` is same as the current one
* */

View File

@ -4,6 +4,7 @@ import TableHeader from '../TableCommon/TableHeader';
import { editItem, E_ONGOING_REQ } from './EditActions';
import globals from '../../../../Globals';
import { modalClose } from './EditActions';
import JsonInput from '../../../Common/CustomInputTypes/JsonInput';
import Button from '../../../Common/Button/Button';
import {
@ -14,8 +15,6 @@ import {
DATE,
BOOLEAN,
UUID,
JSONDTYPE,
JSONB,
TIMESTAMP,
TIMETZ,
} from '../utils';
@ -167,16 +166,19 @@ class EditItem extends Component {
data-test={`typed-input-${i}`}
/>
);
} else if (colType === JSONDTYPE || colType === JSONB) {
} else if (colType === 'json' || colType === 'jsonb') {
const standardEditProps = {
className: `form-control ${styles.insertBox}`,
onClick: clicker,
ref: inputRef,
defaultValue: JSON.stringify(oldItem[colName]),
'data-test': `typed-input-${i}`,
type: 'text',
};
typedInput = (
<input
placeholder={getPlaceholder(colType)}
type="text"
className={'form-control ' + styles.insertBox}
onClick={clicker}
ref={inputRef}
defaultValue={JSON.stringify(oldItem[colName])}
data-test={`typed-input-${i}`}
<JsonInput
standardProps={standardEditProps}
placeholderProp={'{"name": "foo"} or [12, "asdf"]'}
/>
);
} else if (colType === BOOLEAN) {
@ -350,7 +352,10 @@ class EditItem extends Component {
// default
return;
} else {
inputValues[colName] = refs[colName].valueNode.value; // TypedInput is an input inside a div
inputValues[colName] =
refs[colName].valueNode.props !== undefined
? refs[colName].valueNode.props.value
: refs[colName].valueNode.value;
}
});
dispatch(editItem(tableName, inputValues));

View File

@ -4,9 +4,12 @@ import TableHeader from '../TableCommon/TableHeader';
import { insertItem, I_RESET } from './InsertActions';
import { ordinalColSort } from '../utils';
import { setTable } from '../DataActions';
import JsonInput from '../../../Common/CustomInputTypes/JsonInput';
import Button from '../../../Common/Button/Button';
import { getPlaceholder, BOOLEAN, JSONB, JSONDTYPE } from '../utils';
import { getParentNodeByClass } from '../../../../utils/domFunctions';
class InsertItem extends Component {
constructor() {
super();
@ -57,7 +60,12 @@ class InsertItem extends Component {
refs[colName] = { valueNode: null, nullNode: null, defaultNode: null };
const inputRef = node => (refs[colName].valueNode = node);
const clicker = e => {
e.target.parentNode.click();
const checkboxLabel = getParentNodeByClass(e.target, 'radio-inline');
if (checkboxLabel) {
checkboxLabel.click();
} else {
e.target.parentNode.click();
}
e.target.focus();
};
const colDefault = col.column_default;
@ -74,22 +82,21 @@ class InsertItem extends Component {
'data-test': `typed-input-${i}`,
defaultValue: clone && colName in clone ? clone[colName] : '',
onClick: clicker,
onChange: e => {
onChange: (e, val) => {
if (isAutoIncrement) return;
if (!isNullable && !hasDefault) return;
const textValue = e.target.value;
const textValue = typeof val === 'string' ? val : e.target.value;
const radioToSelectWhenEmpty = hasDefault
? refs[colName].defaultNode
: refs[colName].nullNode;
refs[colName].insertRadioNode.checked = !!textValue.length;
radioToSelectWhenEmpty.checked = !textValue.length;
},
onFocus: e => {
if (isAutoIncrement) return;
if (!isNullable && !hasDefault) return;
const textValue = e.target.value;
if (
textValue === undefined ||
@ -110,7 +117,6 @@ class InsertItem extends Component {
};
const colType = col.data_type;
const placeHolder = hasDefault
? col.column_default
: getPlaceholder(colType);
@ -128,12 +134,9 @@ class InsertItem extends Component {
if (colType === JSONDTYPE || colType === JSONB) {
// JSON/JSONB
typedInput = (
<input
{...standardInputProps}
placeholder={placeHolder}
defaultValue={
clone && colName in clone ? JSON.stringify(clone[colName]) : ''
}
<JsonInput
standardProps={standardInputProps}
placeholderProp={getPlaceholder(colType)}
/>
);
}
@ -262,7 +265,10 @@ class InsertItem extends Component {
// default
return;
} else {
inputValues[colName] = refs[colName].valueNode.value;
inputValues[colName] =
refs[colName].valueNode.props !== undefined
? refs[colName].valueNode.props.value
: refs[colName].valueNode.value;
}
});
dispatch(insertItem(tableName, inputValues)).then(() => {

View File

@ -392,42 +392,24 @@ export const commonDataTypes = [
description: 'autoincrementing four-byte integer',
hasuraDatatype: null,
},
{
name: 'UUID',
value: 'uuid',
description: 'universal unique identifier',
hasuraDatatype: 'uuid',
},
{
name: 'Big Integer',
value: 'bigint',
description: 'signed eight-byte integer',
hasuraDatatype: 'bigint',
},
{
name: 'Big Integer (auto-increment)',
value: 'bigserial',
description: 'autoincrementing eight-byte integer',
hasuraDatatype: null,
},
{
name: 'Text',
value: 'text',
description: 'variable-length character string',
hasuraDatatype: 'text',
},
{
name: 'Boolean',
value: 'boolean',
description: 'logical Boolean (true/false)',
hasuraDatatype: 'boolean',
},
{
name: 'Numeric',
value: 'numeric',
description: 'exact numeric of selected precision',
hasuraDatatype: 'numeric',
},
{
name: 'Date',
value: 'date',
description: 'calendar date (year, month, day)',
hasuraDatatype: 'date',
},
{
name: 'Timestamp',
value: 'timestamptz',
@ -441,10 +423,16 @@ export const commonDataTypes = [
hasuraDatatype: 'time with time zone',
},
{
name: 'Boolean',
value: 'boolean',
description: 'logical Boolean (true/false)',
hasuraDatatype: 'boolean',
name: 'Date',
value: 'date',
description: 'calendar date (year, month, day)',
hasuraDatatype: 'date',
},
{
name: 'UUID',
value: 'uuid',
description: 'universal unique identifier',
hasuraDatatype: 'uuid',
},
{
name: 'JSONB',
@ -452,6 +440,18 @@ export const commonDataTypes = [
description: 'binary format JSON data',
hasuraDatatype: 'jsonb',
},
{
name: 'Big Integer',
value: 'bigint',
description: 'signed eight-byte integer',
hasuraDatatype: 'bigint',
},
{
name: 'Big Integer (auto-increment)',
value: 'bigserial',
description: 'autoincrementing eight-byte integer',
hasuraDatatype: null,
},
];
export const fetchColumnTypesQuery = `

View File

@ -8,22 +8,22 @@ export const convertListToDict = list => {
const newList = list instanceof Array ? list : [].concat(list);
return newList.length > 1
? newList.reduce((prev, next) => {
const accumulator =
const accumulator =
prev instanceof Object
? prev
: {
[prev]: 1,
};
return {
...accumulator,
...{
[next]: accumulator[next] ? accumulator[next] + 1 : 1,
},
};
})
: {
[newList[0]]: 1,
[prev]: 1,
};
return {
...accumulator,
...{
[next]: accumulator[next] ? accumulator[next] + 1 : 1,
},
};
})
: {
[newList[0]]: 1,
};
};
/**
@ -37,26 +37,26 @@ export const convertListToDictUsingKV = (keyName, keyValue, list) => {
// if the list has more than one object then reduce it.
return list.length > 1
? list.reduce((prev, next, index) => {
// Initial object for the list.
if (index === 1) {
const newObj = {};
newObj[prev[keyName]] = prev[keyValue];
newObj[next[keyName]] = next[keyValue];
return newObj;
}
// Other objects for the list.
prev[next[keyName]] = next[keyValue];
return prev;
})
// Initial object for the list.
if (index === 1) {
const newObj = {};
newObj[prev[keyName]] = prev[keyValue];
newObj[next[keyName]] = next[keyValue];
return newObj;
}
// Other objects for the list.
prev[next[keyName]] = next[keyValue];
return prev;
})
: (() => {
return list.length <= 0
? {}
: (() => {
const newObj = {};
newObj[list[0][keyName]] = list[0][keyValue];
return newObj;
})();
})();
return list.length <= 0
? {}
: (() => {
const newObj = {};
newObj[list[0][keyName]] = list[0][keyValue];
return newObj;
})();
})();
};
/**

View File

@ -1,9 +1,18 @@
/* Add error messages when trying to use the function */
export const getMeParentNode = (node, selector) => {
export const getParentNodeByAttribute = (node, selector) => {
if (node && !node.documentElement) {
return node.hasAttribute(selector)
? node
: getMeParentNode(node.parentNode, selector);
: getParentNodeByAttribute(node.parentNode, selector);
}
return null;
};
export const getParentNodeByClass = (node, selector) => {
if (node && !node.documentElement) {
return node.classList.contains(selector)
? node
: getParentNodeByClass(node.parentNode, selector);
}
return null;
};