@talend/react-components
Version:
Set of react components.
595 lines (587 loc) • 19.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ComplexItem = exports.ARRAY_ABSTRACT = void 0;
exports.Item = Item;
exports.JSONLike = JSONLike;
exports.LineItem = void 0;
exports.NativeValue = NativeValue;
exports.OBJECT_ABSTRACT = void 0;
exports.abstracter = abstracter;
exports.default = void 0;
exports.getDataAbstract = getDataAbstract;
exports.getDataInfo = getDataInfo;
exports.getJSONPath = getJSONPath;
exports.getName = getName;
var _propTypes = _interopRequireDefault(require("prop-types"));
var _react = require("react");
var _invariant = _interopRequireDefault(require("invariant"));
var _classnames = _interopRequireDefault(require("classnames"));
var _reactI18next = require("react-i18next");
var _Actions = require("../../Actions");
var _TooltipTrigger = _interopRequireDefault(require("../../TooltipTrigger"));
var _JSONLikeModule = _interopRequireDefault(require("./JSONLike.module.scss"));
var _constants = _interopRequireDefault(require("../../constants"));
var _reactA11y = require("@talend/react-a11y");
var _translate = _interopRequireDefault(require("../../translate"));
var _lodash = require("lodash");
var _jsxRuntime = require("react/jsx-runtime");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function noop() {}
const VALIDE_TYPES = ['number', 'string', 'bool'];
const COMPLEX_TYPES = ['object', 'array'];
const ARRAY_ABSTRACT = exports.ARRAY_ABSTRACT = '[...]';
const OBJECT_ABSTRACT = exports.OBJECT_ABSTRACT = '{...}';
const dateTimeISOStringRegexp = new RegExp(/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.*)?(Z)?$/) // eslint-disable-line max-len
;
const dateTimeRegexp = new RegExp(/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z)?$/) // eslint-disable-line max-len
;
const dateRegexp = new RegExp(/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])$/);
const timeRegexp = new RegExp(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/);
function NativeValue({
data,
edit,
className,
onChange,
jsonpath,
wrap,
isValueOverflown
}) {
const type = typeof data;
let display = data;
let inputType = 'number';
if (type === 'boolean') {
display = data.toString();
inputType = 'checkbox';
} else if (type === 'string') {
inputType = 'text';
}
if (edit) {
return /*#__PURE__*/(0, _jsxRuntime.jsx)("input", {
type: inputType,
value: data,
onChange: e => onChange(e, {
jsonpath
})
});
}
const lineValueClasses = (0, _classnames.default)(className, _JSONLikeModule.default.native, _JSONLikeModule.default[type], {
[`${_JSONLikeModule.default['wrap-string']}`]: wrap,
[_JSONLikeModule.default['shrink-value']]: isValueOverflown
});
return /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
className: lineValueClasses,
children: display
});
}
NativeValue.propTypes = {
data: _propTypes.default.oneOfType([_propTypes.default.bool, _propTypes.default.number, _propTypes.default.string]),
edit: _propTypes.default.bool,
className: _propTypes.default.string,
onChange: _propTypes.default.func,
jsonpath: _propTypes.default.string,
wrap: _propTypes.default.bool,
isValueOverflown: _propTypes.default.bool
};
NativeValue.defaultProps = {
className: _JSONLikeModule.default.value
};
/**
* return JSONPath braket notation
* @param {string} key object key
* @param {string} prefix current jsonpath
* @param {string} type one of 'array' or 'object'
* @return {string} jsonpath
*/
function getJSONPath(key, prefix, type) {
if (type === 'array') {
return `${prefix}[${key}]`;
}
return `${prefix}['${key}']`;
}
function getName(name, t) {
if (!name) {
return null;
}
return /*#__PURE__*/(0, _jsxRuntime.jsxs)("span", {
className: _JSONLikeModule.default.key,
children: [name, t('COLON', {
defaultValue: ':'
})]
}, "name");
}
class LineItem extends _react.Component {
getTabIndex(isSelected) {
let shouldBeFocusable = false;
if (isSelected || !this.props.selectedJsonpath && this.props.level === 1 && this.props.index === 1) {
shouldBeFocusable = true;
}
return shouldBeFocusable ? 0 : -1;
}
isSelected() {
const {
selectedJsonpath,
jsonpath
} = this.props;
return selectedJsonpath && selectedJsonpath === jsonpath;
}
render() {
const {
data,
id,
index,
level,
isOpened,
hasChildren,
siblings,
name,
tag,
jsonpath,
onKeyDown,
onSelect,
children,
badge,
icon,
type,
value,
isValueOverflown,
t
} = this.props;
const isSelectedLine = this.isSelected();
const lineClass = (0, _classnames.default)(_JSONLikeModule.default.line, {
[_JSONLikeModule.default['full-width']]: isValueOverflown
});
return /*#__PURE__*/(0, _jsxRuntime.jsxs)("li", {
// eslint-disable-line jsx-a11y/no-static-element-interactions
id: id,
role: "treeitem",
tabIndex: this.getTabIndex(isSelectedLine),
"aria-expanded": hasChildren ? isOpened : undefined,
"aria-level": level,
"aria-posinset": index,
"aria-setsize": siblings ? siblings.length : 1,
"aria-selected": isSelectedLine,
className: _JSONLikeModule.default['list-item'],
onClick: e => {
e.stopPropagation();
onSelect(e, {
jsonpath
});
},
onKeyDown: e => onKeyDown(e, this.ref, {
data,
hasChildren,
isOpened,
jsonpath,
siblings
}),
ref: ref => {
this.ref = ref;
},
children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: lineClass,
children: [icon, /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: (0, _classnames.default)(_JSONLikeModule.default['line-main'], {
[_JSONLikeModule.default['shrink-value']]: isValueOverflown
}),
children: [getName(name, t), value, type && /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
className: `tc-object-viewer-line-type ${_JSONLikeModule.default.type}`,
children: ["(", type, ")"]
}, "type"), badge, tag]
}, "line-main")]
}, "line"), children]
}, id || index);
}
}
exports.LineItem = LineItem;
LineItem.propTypes = {
badge: _propTypes.default.node,
children: _propTypes.default.node,
data: _propTypes.default.any,
hasChildren: _propTypes.default.bool,
icon: _propTypes.default.node,
id: _propTypes.default.string,
index: _propTypes.default.number,
isOpened: _propTypes.default.bool,
jsonpath: _propTypes.default.string,
level: _propTypes.default.number,
name: _propTypes.default.string,
onKeyDown: _propTypes.default.func.isRequired,
onSelect: _propTypes.default.func.isRequired,
selectedJsonpath: _propTypes.default.string,
siblings: _propTypes.default.array,
tag: _propTypes.default.node,
type: _propTypes.default.string,
value: _propTypes.default.node,
isValueOverflown: _propTypes.default.bool,
t: _propTypes.default.func
};
/**
* return Info object of the data
* @param {Object|Array} data The data to display
* @param {string} tupleLabel The label that will replace the 'Object' type displayed
* @return {Object} DataInfo object
*/
function getDataInfo(data, tupleLabel) {
const info = {
type: typeof data,
keys: Object.keys(data)
};
if (VALIDE_TYPES.indexOf(info.type) === -1) {
(0, _invariant.default)(true, `Type ${info.type} is not supported`);
}
if (Array.isArray(data)) {
info.type = 'array';
info.length = data.length;
} else if (info.type === 'object') {
info.keys = Object.keys(data);
info.length = info.keys.length;
if (tupleLabel && tupleLabel.length > 0) {
info.type = tupleLabel;
}
} else if (info.type === 'string') {
if (dateTimeRegexp.test(data)) {
info.type = 'datetime';
} else if (dateRegexp.test(data)) {
info.type = 'date';
} else if (timeRegexp.test(data)) {
info.type = 'time';
} else if (dateTimeISOStringRegexp.test(data)) {
info.type = 'datetime';
}
}
return info;
}
/**
* return The concatenation of already built abstract
* and the native value or complexe types representation
*
* @param {string} acc Accumulator
* @param {any} item Current object|literal of the iteration
* @return {string} The abstract being built
*/
function abstracter(acc, item) {
if (Array.isArray(item)) {
if (acc.length > 0) {
return `${acc}, ${ARRAY_ABSTRACT}`;
}
return ARRAY_ABSTRACT;
} else if (typeof item === 'object') {
if (acc.length > 0) {
return `${acc}, ${OBJECT_ABSTRACT}`;
}
return OBJECT_ABSTRACT;
}
if (acc.length > 0) {
return `${acc}, ${item}`;
}
// interpolation is useful for boolean values
return `${item}`;
}
/**
* return The abstract of and array or object
* with simple representation for complex types
*
* @param {Object|Array} data data to abstract by values
* @return {string} The abstract built
*/
function getDataAbstract(data) {
let abstract = '';
if (Array.isArray(data)) {
abstract = data.reduce((acc, item) => abstracter(acc, item), abstract);
} else if (typeof data === 'object') {
const oKeys = Object.keys(data);
abstract = oKeys.reduce((acc, key) => abstracter(acc, data[key]), abstract);
}
return abstract;
}
function UntranslatedComplexItem(props) {
const {
data,
id,
info,
jsonpath,
level,
opened,
t
} = props;
const isOpened = opened.indexOf(jsonpath) !== -1;
const decoratedLength = info.type === 'array' ? `[${info.length}]` : `(${info.length})`;
let children = null;
if (isOpened) {
const childrenSiblings = info.keys.map(key => ({
key,
data: data[key],
jsonpath: getJSONPath(key, jsonpath, info.type)
}));
children = /*#__PURE__*/(0, _jsxRuntime.jsx)("ul", {
role: "group",
className: _JSONLikeModule.default['complex-item'],
children: info.keys.map((key, i) => {
const childId = id && `${id}-${key}`;
return (
/*#__PURE__*/
// eslint-disable-next-line @typescript-eslint/no-use-before-define
(0, _react.createElement)(Item, {
...props,
key: childId || i,
data: data[key],
id: childId,
index: i + 1,
siblings: childrenSiblings,
jsonpath: getJSONPath(key, jsonpath, info.type),
level: level + 1,
name: key
})
);
})
}, "children-group");
}
const childCount = /*#__PURE__*/(0, _jsxRuntime.jsx)("sup", {
className: `${_JSONLikeModule.default.badge} badge`,
"aria-label": t('TC_OBJECT_VIEWER_NB_CHILD', {
defaultValue: 'Contains {{count}} child object',
defaultValue_other: 'Contains {{count}} child objects',
count: info.length
}),
children: decoratedLength
}, "badge");
return /*#__PURE__*/(0, _jsxRuntime.jsx)(LineItem, {
...props,
hasChildren: true,
isOpened: isOpened,
icon: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Actions.Action, {
className: (0, _classnames.default)(_JSONLikeModule.default.toggle, 'tc-object-viewer-toggle'),
icon: "talend-caret-down",
iconTransform: isOpened ? undefined : 'rotate-270',
id: id && `${id}-toggle`,
onClick: e => {
e.stopPropagation();
props.onToggle(e, {
data,
isOpened,
jsonpath
});
},
label: "",
"aria-hidden": true,
tabIndex: "-1",
link: true
}, "toggle"),
badge: props.hideTooltip ? childCount : /*#__PURE__*/(0, _jsxRuntime.jsx)(_TooltipTrigger.default, {
className: "offset",
label: getDataAbstract(data),
tooltipPlacement: "right",
children: childCount
}, "badge-tooltip"),
type: props.showType ? info.type : null,
children: children
});
}
UntranslatedComplexItem.defaultProps = {
t: (0, _translate.default)()
};
UntranslatedComplexItem.displayName = 'ComplexItem';
UntranslatedComplexItem.propTypes = {
data: _propTypes.default.oneOfType([_propTypes.default.bool, _propTypes.default.number, _propTypes.default.string, _propTypes.default.object, _propTypes.default.array]),
id: _propTypes.default.string,
info: _propTypes.default.shape({
type: _propTypes.default.string,
keys: _propTypes.default.array,
length: _propTypes.default.number
}).isRequired,
jsonpath: _propTypes.default.string,
level: _propTypes.default.number,
onToggle: _propTypes.default.func.isRequired,
opened: _propTypes.default.arrayOf(_propTypes.default.string).isRequired,
showType: _propTypes.default.bool,
hideTooltip: _propTypes.default.bool,
t: _propTypes.default.func
};
/**
* translated complex item is still exported as is, the UntranslatedComplexItem undecorated const
* was kept to avoid import breaking change on client app
* I think translation probably should not have crept in UI/components.
* First it create a strong cooupling between the translation stack and the component stack.
* Second it make UI much more complex.
*
* Anyway the next release that may contains breaking changes would be a good opportunity to
* rename the undecorated component to ComplexItem
* rename the exported variable containing the translated component to TranslatedComplexItem
*
* AxelC
*/
const ComplexItem = exports.ComplexItem = (0, _reactI18next.withTranslation)(_constants.default)(UntranslatedComplexItem);
function Item(props) {
const {
data,
tagged,
jsonpath,
tupleLabel
} = props;
const [lineItemWidth, setLineItemWidth] = (0, _react.useState)(false);
const [nativeValueWrap, setNativeValueWrap] = (0, _react.useState)(false);
const lineItemRef = (0, _react.useCallback)(node => {
if (node) {
if (node.ref.offsetParent.offsetWidth < node.ref.scrollWidth) {
setLineItemWidth(true);
}
}
}, []);
if (tupleLabel) {
COMPLEX_TYPES.push(tupleLabel);
}
const isEdited = props.edited.indexOf(jsonpath) !== -1 && !!props.onChange;
const tag = tagged && tagged[jsonpath];
if (data === undefined || data === null) {
return /*#__PURE__*/(0, _jsxRuntime.jsx)(LineItem, {
...props,
tag: tag
});
}
const info = getDataInfo(data, tupleLabel);
const isNativeType = COMPLEX_TYPES.indexOf(info.type) === -1;
if (isNativeType) {
return /*#__PURE__*/(0, _jsxRuntime.jsx)(LineItem, {
ref: lineItemRef,
...props,
value: /*#__PURE__*/(0, _jsxRuntime.jsx)(NativeValue, {
data: data,
edit: isEdited,
onEdit: props.onEdit,
onChange: props.onChange,
className: props.nativeValueClassName,
wrap: nativeValueWrap,
isValueOverflown: lineItemWidth
}, "value"),
type: props.showType ? info.type : null,
tag: tag,
isValueOverflown: lineItemWidth
// icon is shown when LineItem value is a long field and needs to be wrapped
,
icon: lineItemWidth && /*#__PURE__*/(0, _jsxRuntime.jsx)(_Actions.Action, {
className: (0, _classnames.default)(_JSONLikeModule.default.chevron, {
[_JSONLikeModule.default['chevron-filled']]: nativeValueWrap
}),
icon: "talend-chevron-left",
iconTransform: nativeValueWrap ? 'rotate-90' : 'rotate-270',
onClick: e => {
e.stopPropagation();
setNativeValueWrap(val => !val);
},
label: "",
"aria-hidden": true,
tabIndex: "-1",
link: true
}, "toggle")
});
}
return /*#__PURE__*/(0, _jsxRuntime.jsx)(ComplexItem, {
...props,
tag: tag,
info: info
});
}
Item.propTypes = {
opened: _propTypes.default.arrayOf(_propTypes.default.string),
data: _propTypes.default.oneOfType([_propTypes.default.bool, _propTypes.default.number, _propTypes.default.string, _propTypes.default.object, _propTypes.default.array]),
edited: _propTypes.default.arrayOf(_propTypes.default.string),
jsonpath: _propTypes.default.string,
onEdit: _propTypes.default.func,
onChange: _propTypes.default.func,
nativeValueClassName: _propTypes.default.string,
showType: _propTypes.default.bool,
tagged: _propTypes.default.objectOf(_propTypes.default.node),
tupleLabel: _propTypes.default.string
};
Item.defaultProps = {
opened: [],
edited: [],
jsonpath: '$',
onEdit: noop
};
/**
* display a tree view json like.
* this is an indented list of item where each item render 'id: type #items'
* @param {function} onSubmit Submit callback.
* @param {string} className User classname, set to the container
* @param {object} style User inline style, set to the container
* @param {object} props Rest of react props
*/
function JSONLike({
onSubmit,
className,
style,
...props
}) {
const rootIsObject = (0, _lodash.isObject)(props.data);
let label = null;
let labelId;
if (rootIsObject) {
if (props.rootLabel) {
labelId = props.id && `${props.id}-label` || 'tc-object-viewer-label';
label = props.hideTooltip ? /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
className: _JSONLikeModule.default['root-label-overflow'],
children: props.rootLabel
}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_TooltipTrigger.default, {
label: props.rootLabel,
tooltipPlacement: "right",
children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
className: _JSONLikeModule.default['root-label-overflow'],
children: props.rootLabel
})
}, "label");
}
}
const containerProps = {
id: props.id && `${props.id}-container`,
'data-testid': props.id && `${props.id}-container`,
className: (0, _classnames.default)('tc-object-viewer', _JSONLikeModule.default.container, className),
style
};
const objectTree = [label, /*#__PURE__*/(0, _jsxRuntime.jsx)("ul", {
className: _JSONLikeModule.default['tc-object-viewer-list'],
role: "tree",
"aria-label": props['aria-label'],
"aria-labelledby": labelId,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(Item, {
...props,
id: props.id && `${props.id}-root`,
siblings: [{
data: props.data,
jsonpath: '$'
}],
level: 1,
index: 1
})
}, "tree")];
if (onSubmit) {
return /*#__PURE__*/(0, _jsxRuntime.jsx)("form", {
...containerProps,
onSubmit: event => {
onSubmit(event);
event.preventDefault();
},
children: objectTree
});
}
return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
...containerProps,
children: objectTree
});
}
JSONLike.displayName = 'JSONLike';
JSONLike.propTypes = {
'aria-label': _propTypes.default.string,
id: _propTypes.default.string,
data: _propTypes.default.oneOfType([...VALIDE_TYPES, ...COMPLEX_TYPES].map(t => _propTypes.default[t])),
onSubmit: _propTypes.default.func,
className: _propTypes.default.string,
style: _propTypes.default.object,
rootLabel: _propTypes.default.string,
tupleLabel: _propTypes.default.string,
hideTooltip: _propTypes.default.bool
};
var _default = exports.default = _reactA11y.Gesture.withTreeGesture(JSONLike);
//# sourceMappingURL=JSONLike.component.js.map