feat: dataset editor improvements (#10444)
This commit is contained in:
parent
9c5b0e1c86
commit
fa07506d0d
|
|
@ -55,7 +55,7 @@ describe('Datasource control', () => {
|
|||
cy.get('a').contains('Edit Datasource').click();
|
||||
cy.get(`input[value="${newMetricName}"]`)
|
||||
.closest('tr')
|
||||
.find('.fa-close')
|
||||
.find('.fa-trash')
|
||||
.click();
|
||||
cy.get('.modal-footer button').contains('Save').click();
|
||||
cy.get('.modal-footer button').contains('OK').click();
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ describe('EditableTitle', () => {
|
|||
value: 'new title',
|
||||
},
|
||||
};
|
||||
const editableWrapper = shallow(<EditableTable {...mockProps} />);
|
||||
let editableWrapper = shallow(<EditableTable {...mockProps} />);
|
||||
const notEditableWrapper = shallow(
|
||||
<EditableTable title="my title" onSaveTitle={callback} />,
|
||||
);
|
||||
|
|
@ -60,8 +60,7 @@ describe('EditableTitle', () => {
|
|||
|
||||
describe('should handle change', () => {
|
||||
afterEach(() => {
|
||||
editableWrapper.setState({ title: 'my title' });
|
||||
editableWrapper.setState({ lastTitle: 'my title' });
|
||||
editableWrapper = shallow(<EditableTable {...mockProps} />);
|
||||
});
|
||||
it('should change title', () => {
|
||||
editableWrapper.find('input').simulate('change', mockEvent);
|
||||
|
|
@ -79,8 +78,7 @@ describe('EditableTitle', () => {
|
|||
});
|
||||
afterEach(() => {
|
||||
callback.resetHistory();
|
||||
editableWrapper.setState({ title: 'my title' });
|
||||
editableWrapper.setState({ lastTitle: 'my title' });
|
||||
editableWrapper = shallow(<EditableTable {...mockProps} />);
|
||||
});
|
||||
|
||||
it('default input type should be text', () => {
|
||||
|
|
@ -88,7 +86,7 @@ describe('EditableTitle', () => {
|
|||
});
|
||||
|
||||
it('should trigger callback', () => {
|
||||
editableWrapper.setState({ title: 'new title' });
|
||||
editableWrapper.find('input').simulate('change', mockEvent);
|
||||
editableWrapper.find('input').simulate('blur');
|
||||
expect(editableWrapper.find('input').props().type).toBe('button');
|
||||
expect(callback.callCount).toBe(1);
|
||||
|
|
@ -101,7 +99,6 @@ describe('EditableTitle', () => {
|
|||
expect(callback.callCount).toBe(0);
|
||||
});
|
||||
it('should not save empty title', () => {
|
||||
editableWrapper.setState({ title: '' });
|
||||
editableWrapper.find('input').simulate('blur');
|
||||
expect(editableWrapper.find('input').props().type).toBe('button');
|
||||
expect(editableWrapper.find('input').props().value).toBe('my title');
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { ReactNode } from 'react';
|
||||
import shortid from 'shortid';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import Button from '../components/Button';
|
||||
|
|
@ -25,47 +24,42 @@ import Fieldset from './Fieldset';
|
|||
import { recurseReactClone } from './utils';
|
||||
import './crud.less';
|
||||
|
||||
const propTypes = {
|
||||
collection: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
itemGenerator: PropTypes.func,
|
||||
columnLabels: PropTypes.object,
|
||||
tableColumns: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
itemRenderers: PropTypes.object,
|
||||
allowDeletes: PropTypes.bool,
|
||||
expandFieldset: PropTypes.node,
|
||||
emptyMessage: PropTypes.node,
|
||||
extraButtons: PropTypes.node,
|
||||
allowAddItem: PropTypes.bool,
|
||||
};
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
itemRenderers: {},
|
||||
columnLabels: {},
|
||||
allowDeletes: false,
|
||||
emptyMessage: 'No entries',
|
||||
allowAddItem: false,
|
||||
itemGenerator: () => ({}),
|
||||
expandFieldset: null,
|
||||
extraButtons: null,
|
||||
};
|
||||
const Frame = props => <div className="frame">{props.children}</div>;
|
||||
Frame.propTypes = { children: PropTypes.node };
|
||||
interface CRUDCollectionProps {
|
||||
allowAddItem?: boolean;
|
||||
allowDeletes?: boolean;
|
||||
collection: Array<object>;
|
||||
columnLabels?: object;
|
||||
emptyMessage: ReactNode;
|
||||
expandFieldset: ReactNode;
|
||||
extraButtons: ReactNode;
|
||||
itemGenerator?: () => any;
|
||||
itemRenderers?: any;
|
||||
onChange?: (arg0: any) => void;
|
||||
tableColumns: Array<any>;
|
||||
}
|
||||
|
||||
function createKeyedCollection(arr) {
|
||||
const newArr = arr.map(o => ({
|
||||
interface CRUDCollectionState {
|
||||
collection: object;
|
||||
expandedColumns: object;
|
||||
}
|
||||
|
||||
function createKeyedCollection(arr: Array<object>) {
|
||||
const newArr = arr.map((o: any) => ({
|
||||
...o,
|
||||
id: o.id || shortid.generate(),
|
||||
}));
|
||||
const map = {};
|
||||
newArr.forEach(o => {
|
||||
newArr.forEach((o: any) => {
|
||||
map[o.id] = o;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
export default class CRUDCollection extends React.PureComponent {
|
||||
constructor(props) {
|
||||
export default class CRUDCollection extends React.PureComponent<
|
||||
CRUDCollectionProps,
|
||||
CRUDCollectionState
|
||||
> {
|
||||
constructor(props: CRUDCollectionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
expandedColumns: {},
|
||||
|
|
@ -79,14 +73,14 @@ export default class CRUDCollection extends React.PureComponent {
|
|||
this.renderTableBody = this.renderTableBody.bind(this);
|
||||
this.changeCollection = this.changeCollection.bind(this);
|
||||
}
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps: CRUDCollectionProps) {
|
||||
if (nextProps.collection !== this.props.collection) {
|
||||
this.setState({
|
||||
collection: createKeyedCollection(nextProps.collection),
|
||||
});
|
||||
}
|
||||
}
|
||||
onCellChange(id, col, val) {
|
||||
onCellChange(id: number, col: string, val: boolean) {
|
||||
this.changeCollection({
|
||||
...this.state.collection,
|
||||
[id]: {
|
||||
|
|
@ -96,35 +90,39 @@ export default class CRUDCollection extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
onAddItem() {
|
||||
let newItem = this.props.itemGenerator();
|
||||
if (!newItem.id) {
|
||||
newItem = { ...newItem, id: shortid.generate() };
|
||||
if (this.props.itemGenerator) {
|
||||
let newItem = this.props.itemGenerator();
|
||||
if (!newItem.id) {
|
||||
newItem = { ...newItem, id: shortid.generate() };
|
||||
}
|
||||
this.changeCollection({
|
||||
...this.state.collection,
|
||||
[newItem.id]: newItem,
|
||||
});
|
||||
}
|
||||
this.changeCollection({
|
||||
...this.state.collection,
|
||||
[newItem.id]: newItem,
|
||||
});
|
||||
}
|
||||
onFieldsetChange(item) {
|
||||
onFieldsetChange(item: any) {
|
||||
this.changeCollection({
|
||||
...this.state.collection,
|
||||
[item.id]: item,
|
||||
});
|
||||
}
|
||||
getLabel(col) {
|
||||
getLabel(col: any) {
|
||||
const { columnLabels } = this.props;
|
||||
let label = columnLabels[col] ? columnLabels[col] : col;
|
||||
let label = columnLabels && columnLabels[col] ? columnLabels[col] : col;
|
||||
if (label.startsWith('__')) {
|
||||
// special label-free columns (ie: caret for expand, delete cross)
|
||||
label = '';
|
||||
}
|
||||
return label;
|
||||
}
|
||||
changeCollection(collection) {
|
||||
changeCollection(collection: any) {
|
||||
this.setState({ collection });
|
||||
this.props.onChange(Object.keys(collection).map(k => collection[k]));
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(Object.keys(collection).map(k => collection[k]));
|
||||
}
|
||||
}
|
||||
deleteItem(id) {
|
||||
deleteItem(id: number) {
|
||||
const newColl = { ...this.state.collection };
|
||||
delete newColl[id];
|
||||
this.changeCollection(newColl);
|
||||
|
|
@ -136,7 +134,7 @@ export default class CRUDCollection extends React.PureComponent {
|
|||
: tableColumns;
|
||||
return expandFieldset ? ['__expand'].concat(cols) : cols;
|
||||
}
|
||||
toggleExpand(id) {
|
||||
toggleExpand(id: any) {
|
||||
this.onCellChange(id, '__expanded', false);
|
||||
this.setState({
|
||||
expandedColumns: {
|
||||
|
|
@ -147,19 +145,33 @@ export default class CRUDCollection extends React.PureComponent {
|
|||
}
|
||||
renderHeaderRow() {
|
||||
const cols = this.effectiveTableColumns();
|
||||
const {
|
||||
allowAddItem,
|
||||
allowDeletes,
|
||||
expandFieldset,
|
||||
extraButtons,
|
||||
} = this.props;
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
{this.props.expandFieldset && <th className="tiny-cell" />}
|
||||
{expandFieldset && <th className="tiny-cell" />}
|
||||
{cols.map(col => (
|
||||
<th key={col}>{this.getLabel(col)}</th>
|
||||
))}
|
||||
{this.props.allowDeletes && <th className="tiny-cell" />}
|
||||
{extraButtons}
|
||||
{allowDeletes && !allowAddItem && <th className="tiny-cell" />}
|
||||
{allowAddItem && (
|
||||
<th>
|
||||
<Button bsStyle="primary" onClick={this.onAddItem}>
|
||||
<i className="fa fa-plus" /> {t('Add Item')}
|
||||
</Button>
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
renderExpandableSection(item) {
|
||||
renderExpandableSection(item: any) {
|
||||
const propsGenerator = () => ({ item, onChange: this.onFieldsetChange });
|
||||
return recurseReactClone(
|
||||
this.props.expandFieldset,
|
||||
|
|
@ -167,14 +179,19 @@ export default class CRUDCollection extends React.PureComponent {
|
|||
propsGenerator,
|
||||
);
|
||||
}
|
||||
renderCell(record, col) {
|
||||
const renderer = this.props.itemRenderers[col];
|
||||
renderCell(record: any, col: any) {
|
||||
const renderer = this.props.itemRenderers && this.props.itemRenderers[col];
|
||||
const val = record[col];
|
||||
const onChange = this.onCellChange.bind(this, record.id, col);
|
||||
return renderer ? renderer(val, onChange, this.getLabel(col)) : val;
|
||||
}
|
||||
renderItem(record) {
|
||||
const { tableColumns, allowDeletes, expandFieldset } = this.props;
|
||||
renderItem(record: any) {
|
||||
const {
|
||||
allowAddItem,
|
||||
allowDeletes,
|
||||
expandFieldset,
|
||||
tableColumns,
|
||||
} = this.props;
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
const isExpanded =
|
||||
!!this.state.expandedColumns[record.id] || record.__expanded;
|
||||
|
|
@ -198,13 +215,16 @@ export default class CRUDCollection extends React.PureComponent {
|
|||
<td key={col}>{this.renderCell(record, col)}</td>
|
||||
)),
|
||||
);
|
||||
if (allowAddItem) {
|
||||
tds.push(<td />);
|
||||
}
|
||||
if (allowDeletes) {
|
||||
tds.push(
|
||||
<td key="__actions">
|
||||
<i
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="fa fa-close text-primary pointer"
|
||||
className="fa fa-trash text-primary pointer"
|
||||
onClick={this.deleteItem.bind(this, record.id)}
|
||||
/>
|
||||
</td>,
|
||||
|
|
@ -252,17 +272,7 @@ export default class CRUDCollection extends React.PureComponent {
|
|||
{this.renderHeaderRow()}
|
||||
{this.renderTableBody()}
|
||||
</table>
|
||||
<div>
|
||||
{this.props.allowAddItem && (
|
||||
<Button bsStyle="primary" onClick={this.onAddItem}>
|
||||
<i className="fa fa-plus" /> {t('Add Item')}
|
||||
</Button>
|
||||
)}
|
||||
{this.props.extraButtons}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
CRUDCollection.defaultProps = defaultProps;
|
||||
CRUDCollection.propTypes = propTypes;
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import TooltipWrapper from './TooltipWrapper';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string,
|
||||
canEdit: PropTypes.bool,
|
||||
multiLine: PropTypes.bool,
|
||||
onSaveTitle: PropTypes.func,
|
||||
noPermitTooltip: PropTypes.string,
|
||||
showTooltip: PropTypes.bool,
|
||||
emptyText: PropTypes.node,
|
||||
style: PropTypes.object,
|
||||
extraClasses: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
PropTypes.string,
|
||||
]),
|
||||
};
|
||||
const defaultProps = {
|
||||
title: t('Title'),
|
||||
canEdit: false,
|
||||
multiLine: false,
|
||||
showTooltip: true,
|
||||
onSaveTitle: () => {},
|
||||
emptyText: '<empty>',
|
||||
style: null,
|
||||
extraClasses: null,
|
||||
};
|
||||
|
||||
export default class EditableTitle extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
title: this.props.title,
|
||||
lastTitle: this.props.title,
|
||||
};
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
|
||||
// Used so we can access the DOM element if a user clicks on this component.
|
||||
this.contentRef = React.createRef();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.title !== this.state.title) {
|
||||
this.setState({
|
||||
lastTitle: this.state.title,
|
||||
title: nextProps.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
if (!this.props.canEdit || this.state.isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For multi-line values, save the actual rendered size of the displayed text.
|
||||
// Later, if a textarea is constructed for editing the value, we'll need this.
|
||||
const contentBoundingRect = this.contentRef.current
|
||||
? this.contentRef.current.getBoundingClientRect()
|
||||
: null;
|
||||
|
||||
this.setState({ isEditing: true, contentBoundingRect });
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
const title = this.state.title.trim();
|
||||
|
||||
if (!this.props.canEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isEditing: false,
|
||||
});
|
||||
|
||||
if (!title.length) {
|
||||
this.setState({
|
||||
title: this.state.lastTitle,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.lastTitle !== title) {
|
||||
this.setState({
|
||||
lastTitle: title,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.title !== title) {
|
||||
this.props.onSaveTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
// this entire method exists to support using EditableTitle as the title of a
|
||||
// react-bootstrap Tab, as a workaround for this line in react-bootstrap https://goo.gl/ZVLmv4
|
||||
//
|
||||
// tl;dr when a Tab EditableTitle is being edited, typically the Tab it's within has been
|
||||
// clicked and is focused/active. for accessibility, when focused the Tab <a /> intercepts
|
||||
// the ' ' key (among others, including all arrows) and onChange() doesn't fire. somehow
|
||||
// keydown is still called so we can detect this and manually add a ' ' to the current title
|
||||
handleKeyDown(event) {
|
||||
if (event.key === ' ') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(ev) {
|
||||
if (!this.props.canEdit) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
title: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyPress(ev) {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
this.handleBlur();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isEditing, title, contentBoundingRect } = this.state;
|
||||
const {
|
||||
emptyText,
|
||||
multiLine,
|
||||
showTooltip,
|
||||
canEdit,
|
||||
noPermitTooltip,
|
||||
style,
|
||||
extraClasses,
|
||||
} = this.props;
|
||||
|
||||
let value;
|
||||
if (title) {
|
||||
value = title;
|
||||
} else if (!isEditing) {
|
||||
value = emptyText;
|
||||
}
|
||||
|
||||
// Construct an inline style based on previously-saved height of the rendered label. Only
|
||||
// used in multi-line contexts.
|
||||
const editStyle =
|
||||
isEditing && contentBoundingRect
|
||||
? { height: `${contentBoundingRect.height}px` }
|
||||
: null;
|
||||
|
||||
// Create a textarea when we're editing a multi-line value, otherwise create an input (which may
|
||||
// be text or a button).
|
||||
let input =
|
||||
multiLine && isEditing ? (
|
||||
<textarea
|
||||
ref={this.contentRef}
|
||||
required
|
||||
value={value}
|
||||
className={!title ? 'text-muted' : null}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
onClick={this.handleClick}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
style={editStyle}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
ref={this.contentRef}
|
||||
required
|
||||
type={isEditing ? 'text' : 'button'}
|
||||
value={value}
|
||||
className={!title ? 'text-muted' : null}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
onClick={this.handleClick}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
/>
|
||||
);
|
||||
if (showTooltip && !isEditing) {
|
||||
input = (
|
||||
<TooltipWrapper
|
||||
label="title"
|
||||
tooltip={
|
||||
canEdit
|
||||
? t('click to edit')
|
||||
: noPermitTooltip ||
|
||||
t("You don't have the rights to alter this title.")
|
||||
}
|
||||
>
|
||||
{input}
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={cx(
|
||||
'editable-title',
|
||||
extraClasses,
|
||||
canEdit && 'editable-title--editable',
|
||||
isEditing && 'editable-title--editing',
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{input}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
EditableTitle.propTypes = propTypes;
|
||||
EditableTitle.defaultProps = defaultProps;
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import TooltipWrapper from './TooltipWrapper';
|
||||
|
||||
interface EditableTitleProps {
|
||||
canEdit?: boolean;
|
||||
emptyText?: string;
|
||||
extraClasses?: Array<string> | string;
|
||||
multiLine?: boolean;
|
||||
noPermitTooltip?: string;
|
||||
onSaveTitle: (arg0: string) => {};
|
||||
showTooltip?: boolean;
|
||||
style?: object;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function EditableTitle({
|
||||
canEdit = false,
|
||||
emptyText,
|
||||
extraClasses,
|
||||
multiLine = false,
|
||||
noPermitTooltip,
|
||||
onSaveTitle,
|
||||
showTooltip = true,
|
||||
style,
|
||||
title,
|
||||
}: EditableTitleProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [currentTitle, setCurrentTitle] = useState(title);
|
||||
const [lastTitle, setLastTitle] = useState(title);
|
||||
const [
|
||||
contentBoundingRect,
|
||||
setContentBoundingRect,
|
||||
] = useState<DOMRect | null>(null);
|
||||
// Used so we can access the DOM element if a user clicks on this component.
|
||||
|
||||
const contentRef = useRef<any | HTMLInputElement | HTMLTextAreaElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentTitle !== lastTitle) {
|
||||
setLastTitle(currentTitle);
|
||||
setCurrentTitle(title);
|
||||
}
|
||||
}, [title]);
|
||||
|
||||
function handleClick() {
|
||||
if (!canEdit || isEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For multi-line values, save the actual rendered size of the displayed text.
|
||||
// Later, if a textarea is constructed for editing the value, we'll need this.
|
||||
const contentBounding = contentRef.current
|
||||
? contentRef.current.getBoundingClientRect()
|
||||
: null;
|
||||
setIsEditing(true);
|
||||
setContentBoundingRect(contentBounding);
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
const formattedTitle = currentTitle.trim();
|
||||
|
||||
if (!canEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
|
||||
if (!formattedTitle.length) {
|
||||
setCurrentTitle(lastTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastTitle !== formattedTitle) {
|
||||
setLastTitle(formattedTitle);
|
||||
}
|
||||
|
||||
if (title !== formattedTitle) {
|
||||
onSaveTitle(formattedTitle);
|
||||
}
|
||||
}
|
||||
|
||||
// this entire method exists to support using EditableTitle as the title of a
|
||||
// react-bootstrap Tab, as a workaround for this line in react-bootstrap https://goo.gl/ZVLmv4
|
||||
//
|
||||
// tl;dr when a Tab EditableTitle is being edited, typically the Tab it's within has been
|
||||
// clicked and is focused/active. for accessibility, when focused the Tab <a /> intercepts
|
||||
// the ' ' key (among others, including all arrows) and onChange() doesn't fire. somehow
|
||||
// keydown is still called so we can detect this and manually add a ' ' to the current title
|
||||
function handleKeyDown(event: any) {
|
||||
if (event.key === ' ') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(ev: any) {
|
||||
if (!canEdit) {
|
||||
return;
|
||||
}
|
||||
setCurrentTitle(ev.target.value);
|
||||
}
|
||||
|
||||
function handleKeyPress(ev: any) {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
handleBlur();
|
||||
}
|
||||
}
|
||||
|
||||
let value: string | undefined;
|
||||
if (currentTitle) {
|
||||
value = currentTitle;
|
||||
} else if (!isEditing) {
|
||||
value = emptyText;
|
||||
}
|
||||
|
||||
// Construct an inline style based on previously-saved height of the rendered label. Only
|
||||
// used in multi-line contexts.
|
||||
const editStyle =
|
||||
isEditing && contentBoundingRect
|
||||
? { height: `${contentBoundingRect.height}px` }
|
||||
: undefined;
|
||||
|
||||
// Create a textarea when we're editing a multi-line value, otherwise create an input (which may
|
||||
// be text or a button).
|
||||
let input =
|
||||
multiLine && isEditing ? (
|
||||
<textarea
|
||||
ref={contentRef}
|
||||
required
|
||||
value={value}
|
||||
className={!title ? 'text-muted' : undefined}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onClick={handleClick}
|
||||
onKeyPress={handleKeyPress}
|
||||
style={editStyle}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
ref={contentRef}
|
||||
required
|
||||
type={isEditing ? 'text' : 'button'}
|
||||
value={value}
|
||||
className={!title ? 'text-muted' : undefined}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
onClick={handleClick}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
);
|
||||
if (showTooltip && !isEditing) {
|
||||
input = (
|
||||
<TooltipWrapper
|
||||
label="title"
|
||||
tooltip={
|
||||
canEdit
|
||||
? t('click to edit')
|
||||
: noPermitTooltip ||
|
||||
t("You don't have the rights to alter this title.")
|
||||
}
|
||||
>
|
||||
{input}
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span
|
||||
className={cx(
|
||||
'editable-title',
|
||||
extraClasses,
|
||||
canEdit && 'editable-title--editable',
|
||||
isEditing && 'editable-title--editing',
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{input}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
@ -98,18 +98,23 @@ function ColumnCollectionTable({
|
|||
<Field
|
||||
fieldKey="expression"
|
||||
label={t('SQL Expression')}
|
||||
control={<TextControl />}
|
||||
control={
|
||||
<TextAreaControl
|
||||
language="markdown"
|
||||
offerEditInModal={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Field
|
||||
fieldKey="verbose_name"
|
||||
label={t('Label')}
|
||||
control={<TextControl />}
|
||||
control={<TextControl placeholder={t('Label')} />}
|
||||
/>
|
||||
<Field
|
||||
fieldKey="description"
|
||||
label={t('Description')}
|
||||
control={<TextControl />}
|
||||
control={<TextControl placeholder={t('Description')} />}
|
||||
/>
|
||||
{allowEditDataType && (
|
||||
<Field
|
||||
|
|
@ -145,7 +150,7 @@ function ColumnCollectionTable({
|
|||
database/column name level via the extra parameter.`)}
|
||||
</div>
|
||||
}
|
||||
control={<TextControl />}
|
||||
control={<TextControl placeholder={'%y/%m/%d'} />}
|
||||
/>
|
||||
</Fieldset>
|
||||
</FormContainer>
|
||||
|
|
@ -603,12 +608,12 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
<Field
|
||||
fieldKey="description"
|
||||
label={t('Description')}
|
||||
control={<TextControl />}
|
||||
control={<TextControl placeholder={t('Description')} />}
|
||||
/>
|
||||
<Field
|
||||
fieldKey="d3format"
|
||||
label={t('D3 Format')}
|
||||
control={<TextControl />}
|
||||
control={<TextControl placeholder="%y/%m/%d" />}
|
||||
/>
|
||||
<Field
|
||||
label={t('Warning Message')}
|
||||
|
|
@ -616,7 +621,7 @@ export class DatasourceEditor extends React.PureComponent {
|
|||
description={t(
|
||||
'Warning message to display in the metric selector',
|
||||
)}
|
||||
control={<TextControl />}
|
||||
control={<TextControl placeholder={t('Warning Message')} />}
|
||||
/>
|
||||
</Fieldset>
|
||||
</FormContainer>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup, FormControl } from 'react-bootstrap';
|
||||
import {
|
||||
legacyValidateNumber,
|
||||
|
|
@ -25,30 +24,22 @@ import {
|
|||
} from '@superset-ui/validator';
|
||||
import ControlHeader from '../ControlHeader';
|
||||
|
||||
const propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
isFloat: PropTypes.bool,
|
||||
isInt: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
interface TextControlProps {
|
||||
disabled: boolean;
|
||||
isFloat: boolean;
|
||||
isInt: boolean;
|
||||
onChange: (value: any, errors: any) => {};
|
||||
onFocus: () => {};
|
||||
placeholder: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
onChange: () => {},
|
||||
onFocus: () => {},
|
||||
value: '',
|
||||
isInt: false,
|
||||
isFloat: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export default class TextControl extends React.Component {
|
||||
constructor(props) {
|
||||
export default class TextControl extends React.Component<TextControlProps> {
|
||||
constructor(props: TextControlProps) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
onChange(event) {
|
||||
onChange(event: any) {
|
||||
let value = event.target.value;
|
||||
|
||||
// Validation & casting
|
||||
|
|
@ -83,7 +74,7 @@ export default class TextControl extends React.Component {
|
|||
<FormGroup controlId="formInlineName" bsSize="small">
|
||||
<FormControl
|
||||
type="text"
|
||||
placeholder=""
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.onChange}
|
||||
onFocus={this.props.onFocus}
|
||||
value={value}
|
||||
|
|
@ -94,6 +85,3 @@ export default class TextControl extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
TextControl.propTypes = propTypes;
|
||||
TextControl.defaultProps = defaultProps;
|
||||
|
|
@ -233,6 +233,7 @@ table.table-no-hover tr:hover {
|
|||
}
|
||||
|
||||
.editable-title.datasource-sql-expression textarea {
|
||||
min-height: 100px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue