UNPKG

@talend/react-components

Version:
595 lines (587 loc) 19.5 kB
"use strict"; 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