diff --git a/src/components/BrowserFilter/BrowserFilter.scss b/src/components/BrowserFilter/BrowserFilter.scss
index be017aa56a..55ad5e5fa5 100644
--- a/src/components/BrowserFilter/BrowserFilter.scss
+++ b/src/components/BrowserFilter/BrowserFilter.scss
@@ -22,6 +22,15 @@
&:hover svg {
fill: white;
}
+
+ &.disabled {
+ cursor: not-allowed;
+ color: #66637A;
+
+ &:hover svg {
+ fill: #66637A;
+ }
+ }
}
.entry.active {
diff --git a/src/components/BrowserRow/BrowserRow.react.js b/src/components/BrowserRow/BrowserRow.react.js
index d77d3f223d..367decb2ed 100644
--- a/src/components/BrowserRow/BrowserRow.react.js
+++ b/src/components/BrowserRow/BrowserRow.react.js
@@ -19,8 +19,21 @@ export default class BrowserRow extends Component {
}
render() {
- const { className, columns, currentCol, isUnique, obj, onPointerClick, order, readOnlyFields, row, rowWidth, selection, selectRow, setCopyableValue, setCurrent, setEditing, setRelation, onEditSelectedRow, setContextMenu, onFilterChange, markRequiredField, requiredColumnFields } = this.props;
+ const { className, columns, currentCol, isUnique, obj, onPointerClick, order, readOnlyFields, row, rowWidth, selection, selectRow, setCopyableValue, setCurrent, setEditing, setRelation, onEditSelectedRow, setContextMenu, onFilterChange, markRequiredFieldRow } = this.props;
let attributes = obj.attributes;
+ let requiredCols = [];
+ Object.entries(columns).reduce((acc, cur) => {
+ if (cur[1].required) {
+ acc.push(cur[0]);
+ }
+ return acc;
+ }, requiredCols);
+ // for dynamically changing required field on _User class
+ if (obj.className === '_User' && (obj.get('username') !== undefined || obj.get('password') !== undefined)) {
+ requiredCols = ['username', 'password'];
+ } else if (obj.className === '_User' && obj.get('authData') !== undefined) {
+ requiredCols = ['authData'];
+ }
return (
@@ -58,7 +71,7 @@ export default class BrowserRow extends Component {
hidden = true;
}
}
- let isRequired = requiredColumnFields && requiredColumnFields.includes(name);
+ let isRequired = requiredCols.includes(name);
return (
diff --git a/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js b/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js
index b4e6df4280..beb4778739 100644
--- a/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js
+++ b/src/components/ColumnsConfiguration/ColumnsConfiguration.react.js
@@ -48,13 +48,19 @@ export default class ColumnsConfiguration extends React.Component {
}
render() {
- const { handleColumnDragDrop, handleColumnsOrder, order } = this.props;
- const [ title, entry ] = [styles.title, styles.entry ].map(className => (
+ const { handleColumnDragDrop, handleColumnsOrder, order, disabled } = this.props;
+ let [ title, entry ] = [styles.title, styles.entry ].map(className => (
Manage Columns
));
+ if (disabled) {
+ entry =
+
+ Manage Columns
+
;
+ }
let popover = null;
if (this.state.open) {
diff --git a/src/components/ColumnsConfiguration/ColumnsConfiguration.scss b/src/components/ColumnsConfiguration/ColumnsConfiguration.scss
index 5efb6b55e8..eb3220140d 100644
--- a/src/components/ColumnsConfiguration/ColumnsConfiguration.scss
+++ b/src/components/ColumnsConfiguration/ColumnsConfiguration.scss
@@ -12,6 +12,15 @@
&:hover svg {
fill: white;
}
+
+ &.disabled {
+ cursor: not-allowed;
+ color: #66637A;
+
+ &:hover svg {
+ fill: #66637A;
+ }
+ }
}
.title {
diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js
index e7a8faf11e..d680135f52 100644
--- a/src/dashboard/Data/Browser/Browser.react.js
+++ b/src/dashboard/Data/Browser/Browser.react.js
@@ -71,6 +71,7 @@ class Browser extends DashboardView {
data: null,
lastMax: -1,
newObject: null,
+ editCloneRows: null,
lastError: null,
lastNote: null,
@@ -80,7 +81,7 @@ class Browser extends DashboardView {
isUnique: false,
uniqueField: null,
keepAddingCols: false,
- markRequiredField: false,
+ markRequiredFieldRow: 0,
requiredColumnFields: [],
useMasterKey: true,
@@ -131,9 +132,12 @@ class Browser extends DashboardView {
this.closeEditRowDialog = this.closeEditRowDialog.bind(this);
this.handleShowAcl = this.handleShowAcl.bind(this);
this.onDialogToggle = this.onDialogToggle.bind(this);
+ this.addEditCloneRows = this.addEditCloneRows.bind(this);
this.abortAddRow = this.abortAddRow.bind(this);
this.saveNewRow = this.saveNewRow.bind(this);
- this.setRequiredColumnFields = this.setRequiredColumnFields.bind(this);
+ this.saveEditCloneRow = this.saveEditCloneRow.bind(this);
+ this.abortEditCloneRow = this.abortEditCloneRow.bind(this);
+ this.cancelPendingEditRows = this.cancelPendingEditRows.bind(this);
}
componentWillMount() {
@@ -347,10 +351,6 @@ class Browser extends DashboardView {
}
addRow() {
- if (this.props.params.className === '_User') {
- // if User class row, then reload requiredFields
- this.setRequiredColumnFields();
- }
if (!this.state.newObject) {
const relation = this.state.relation;
this.setState({
@@ -367,9 +367,9 @@ class Browser extends DashboardView {
newObject: null
});
}
- if (this.state.markRequiredField) {
+ if (this.state.markRequiredFieldRow !== 0) {
this.setState({
- markRequiredField: false
+ markRequiredFieldRow: 0
});
}
}
@@ -409,15 +409,15 @@ class Browser extends DashboardView {
if (!obj.get(name)) {
this.showNote("Please enter all required fields", true);
this.setState({
- markRequiredField: true
+ markRequiredFieldRow: -1
});
return;
}
}
}
- if (this.state.markRequiredField) {
+ if (this.state.markRequiredFieldRow) {
this.setState({
- markRequiredField: false
+ markRequiredFieldRow: 0
});
}
obj.save(null, { useMasterKey }).then(
@@ -474,12 +474,117 @@ class Browser extends DashboardView {
);
}
+ saveEditCloneRow(rowIndex) {
+ let obj;
+ if (rowIndex < -1) {
+ obj = this.state.editCloneRows[
+ rowIndex + (this.state.editCloneRows.length + 1)
+ ];
+ }
+ if (!obj) {
+ return;
+ }
+
+ // check if required fields are missing
+ const className = this.props.params.className;
+ let requiredCols = [];
+ if (className) {
+ let classColumns = this.props.schema.data.get('classes').get(className);
+ classColumns.forEach(({ required }, name) => {
+ if (name === 'objectId' || this.state.isUnique && name !== this.state.uniqueField) {
+ return;
+ }
+ if (!!required) {
+ requiredCols.push(name);
+ }
+ if (className === '_User' && (name === 'username' || name === 'password')) {
+ if (!obj.get('authData')) {
+ requiredCols.push(name);
+ }
+ }
+ if (className === '_Role' && (name === 'name' || name === 'ACL')) {
+ requiredCols.push(name);
+ }
+ });
+ }
+ if (requiredCols.length) {
+ for (let idx = 0; idx < requiredCols.length; idx++) {
+ const name = requiredCols[idx];
+ if (!obj.get(name)) {
+ this.showNote("Please enter all required fields", true);
+ this.setState({
+ markRequiredFieldRow: rowIndex
+ });
+ return;
+ }
+ }
+ }
+ if (this.state.markRequiredFieldRow) {
+ this.setState({
+ markRequiredFieldRow: 0
+ });
+ }
+
+ obj.save(null, { useMasterKey: true }).then((objectSaved) => {
+ let msg = objectSaved.className + ' with id \'' + objectSaved.id + '\' ' + 'created';
+ this.showNote(msg, false);
+
+ const state = { data: this.state.data, editCloneRows: this.state.editCloneRows };
+ state.editCloneRows = state.editCloneRows.filter(
+ cloneObj => cloneObj._localId !== obj._localId
+ );
+ if (state.editCloneRows.length === 0) state.editCloneRows = null;
+ if (this.props.params.className === obj.className) {
+ this.state.data.unshift(obj);
+ }
+ this.state.counts[obj.className] += 1;
+ this.setState(state);
+ }, (error) => {
+ let msg = typeof error === 'string' ? error : error.message;
+ if (msg) {
+ msg = msg[0].toUpperCase() + msg.substr(1);
+ }
+
+ this.showNote(msg, true);
+ });
+ }
+
+ abortEditCloneRow(rowIndex) {
+ let obj;
+ if (rowIndex < -1) {
+ obj = this.state.editCloneRows[
+ rowIndex + (this.state.editCloneRows.length + 1)
+ ];
+ }
+ if (!obj) {
+ return;
+ }
+ const state = { editCloneRows: this.state.editCloneRows };
+ state.editCloneRows = state.editCloneRows.filter(
+ cloneObj => cloneObj._localId !== obj._localId
+ );
+ if (state.editCloneRows.length === 0) state.editCloneRows = null;
+ this.setState(state);
+ }
+
addRowWithModal() {
this.addRow();
this.selectRow(undefined, true);
this.showEditRowDialog();
}
+ cancelPendingEditRows() {
+ this.setState({
+ editCloneRows: null
+ });
+ }
+
+ addEditCloneRows(cloneRows) {
+ this.setState({
+ editCloneRows: cloneRows
+ });
+ }
+
removeColumn(name) {
let payload = {
className: this.props.params.className,
@@ -510,6 +615,7 @@ class Browser extends DashboardView {
newObject: null,
lastMax: -1,
selection: {},
+ editCloneRows: null,
};
if (relation) {
await this.setState(initialState);
@@ -593,30 +699,6 @@ class Browser extends DashboardView {
delete filteredCounts[source];
}
this.setState({ data: data, filters, lastMax: MAX_ROWS_FETCHED , filteredCounts: filteredCounts});
- this.setRequiredColumnFields();
- }
-
- setRequiredColumnFields() {
- if (!this.props.schema.data.get('classes')) {
- return;
- }
- let classes = this.props.schema.data.get('classes');
- const { className } = this.props.params;
- let requiredCols = [];
- classes.get(className).forEach(({ required }, name) => {
- if (!!required) {
- requiredCols.push(name);
- }
- if (className === '_User' && (name === 'username' || name === 'password' || name === 'authData')) {
- requiredCols.push(name);
- }
- if (className === '_Role' && (name === 'name' || name === 'ACL')) {
- requiredCols.push(name);
- }
- });
- this.setState({
- requiredColumnFields: requiredCols
- });
}
async fetchRelation(relation, filters = new List()) {
@@ -757,8 +839,12 @@ class Browser extends DashboardView {
}
updateRow(row, attr, value) {
- const isNewObject = row < 0;
- const obj = isNewObject ? this.state.newObject : this.state.data[row];
+ let isNewObject = row === -1;
+ let isEditCloneObj = row < -1;
+ let obj = isNewObject ? this.state.newObject : this.state.data[row];
+ if(isEditCloneObj){
+ obj = this.state.editCloneRows[row + (this.state.editCloneRows.length + 1)];
+ }
if (!obj) {
return;
}
@@ -773,42 +859,85 @@ class Browser extends DashboardView {
}
if (isNewObject) {
- // for dynamically changing required placeholder text for _User class new row object
- if (obj.className === '_User' && attr === 'authData' && value !== undefined) {
- // username & password are not required
- this.setState({
- requiredColumnFields: this.state.requiredColumnFields.filter(field => field !== 'username' && field !== 'password')
- })
- }
-
- if (obj.className === '_User' && (attr === 'username' || attr === 'password') && value !== undefined) {
- // authData is not required
- this.setState({
- requiredColumnFields: this.state.requiredColumnFields.filter(field => field !== 'authData')
- })
- }
-
- if (obj.className === '_User' && obj.get('username') === undefined && obj.get('password') === undefined && obj.get('authData') === undefined) {
- this.setRequiredColumnFields();
- }
-
this.setState({
isNewObject: obj
});
return;
}
+ if (isEditCloneObj) {
+ const editObjIndex = row + (this.state.editCloneRows.length + 1);
+ let cloneRows = [...this.state.editCloneRows];
+ cloneRows.splice(editObjIndex, 1, obj);
+ this.setState({
+ editCloneRows: cloneRows
+ });
+ return;
+ }
const { useMasterKey } = this.state;
obj.save(null, { useMasterKey }).then((objectSaved) => {
let msg = objectSaved.className + ' with id \'' + objectSaved.id + '\' updated';
this.showNote(msg, false);
- const state = { data: this.state.data };
+
+ const state = { data: this.state.data, editCloneRows: this.state.editCloneRows };
+
+ if (isNewObject) {
+ const relation = this.state.relation;
+ if (relation) {
+ const parent = relation.parent;
+ const parentRelation = parent.relation(relation.key);
+ parentRelation.add(obj);
+ const targetClassName = relation.targetClassName;
+ parent.save(null, { useMasterKey: true }).then(() => {
+ this.setState({
+ newObject: null,
+ data: [
+ obj,
+ ...this.state.data,
+ ],
+ relationCount: this.state.relationCount + 1,
+ counts: {
+ ...this.state.counts,
+ [targetClassName]: this.state.counts[targetClassName] + 1,
+ },
+ });
+ }, (error) => {
+ let msg = typeof error === 'string' ? error : error.message;
+ if (msg) {
+ msg = msg[0].toUpperCase() + msg.substr(1);
+ }
+ obj.set(attr, prev);
+ this.setState({ data: this.state.data });
+ this.showNote(msg, true);
+ });
+ } else {
+ state.newObject = null;
+ if (this.props.params.className === obj.className) {
+ this.state.data.unshift(obj);
+ }
+ this.state.counts[obj.className] += 1;
+ }
+ }
+ if (isEditCloneObj) {
+ state.editCloneRows = state.editCloneRows.filter(
+ cloneObj => cloneObj._localId !== obj._localId
+ );
+ if (state.editCloneRows.length === 0) state.editCloneRows = null;
+ if (this.props.params.className === obj.className) {
+ this.state.data.unshift(obj);
+ }
+ this.state.counts[obj.className] += 1;
+ }
this.setState(state);
}, (error) => {
let msg = typeof error === 'string' ? error : error.message;
if (msg) {
msg = msg[0].toUpperCase() + msg.substr(1);
}
+ if (!isNewObject && !isEditCloneObj) {
+ obj.set(attr, prev);
+ this.setState({ data: this.state.data });
+ }
this.showNote(msg, true);
});
@@ -1036,7 +1165,12 @@ class Browser extends DashboardView {
const objects = await query.find({ useMasterKey });
const toClone = [];
for (const object of objects) {
- toClone.push(object.clone());
+ let clonedObj = object.clone();
+ if (className === '_User') {
+ clonedObj.set('username', undefined);
+ clonedObj.set('authData', undefined);
+ }
+ toClone.push(clonedObj);
}
try {
await Parse.Object.saveAll(toClone, { useMasterKey });
@@ -1050,8 +1184,27 @@ class Browser extends DashboardView {
}
});
} catch (error) {
+ //for duplicate, username missing or required field missing errors
+ if (error.code === 137 || error.code === 200 || error.code === 142) {
+ let failedSaveObj = [];
+ let savedObjects = [];
+ toClone.forEach(cloneObj => {
+ cloneObj.dirty()
+ ? failedSaveObj.push(cloneObj)
+ : savedObjects.push(cloneObj);
+ });
+ if (savedObjects.length) {
+ this.setState({
+ data: [...savedObjects, ...this.state.data],
+ counts: {
+ ...this.state.counts,
+ [className]: this.state.counts[className] + savedObjects.length
+ }
+ });
+ }
+ this.addEditCloneRows(failedSaveObj);
+ }
this.setState({
- selection: {},
showCloneSelectedRowsDialog: false
});
this.showNote(error.message, true);
@@ -1192,11 +1345,17 @@ class Browser extends DashboardView {
if (this.state.isUnique) {
columns = {};
}
- classes.get(className).forEach(({ type, targetClass }, name) => {
+ classes.get(className).forEach(({ type, targetClass, required }, name) => {
if (name === 'objectId' || this.state.isUnique && name !== this.state.uniqueField) {
return;
}
- const info = { type };
+ const info = { type, required: !!required };
+ if (className === '_User' && (name === 'username' || name === 'password' || name === 'authData')) {
+ info.required = true;
+ }
+ if (className === '_Role' && (name === 'name' || name === 'ACL')) {
+ info.required = true;
+ }
if (targetClass) {
info.targetClass = targetClass;
}
@@ -1236,12 +1395,16 @@ class Browser extends DashboardView {
onEditPermissions={this.onDialogToggle}
onSaveNewRow={this.saveNewRow}
onAbortAddRow={this.abortAddRow}
+ onSaveEditCloneRow={this.saveEditCloneRow}
+ onAbortEditCloneRow={this.abortEditCloneRow}
+ onCancelPendingEditRows={this.cancelPendingEditRows}
+
currentUser={this.state.currentUser}
useMasterKey={this.state.useMasterKey}
login={this.login}
logout={this.logout}
toggleMasterKeyUsage={this.toggleMasterKeyUsage}
- markRequiredField={this.state.markRequiredField}
+ markRequiredFieldRow={this.state.markRequiredFieldRow}
requiredColumnFields={this.state.requiredColumnFields}
columns={columns}
className={className}
@@ -1252,6 +1415,7 @@ class Browser extends DashboardView {
data={this.state.data}
ordering={this.state.ordering}
newObject={this.state.newObject}
+ editCloneRows={this.state.editCloneRows}
relation={this.state.relation}
disableKeyControls={this.hasExtras()}
updateRow={this.updateRow}
diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js
index a893eacbc9..fb110a5281 100644
--- a/src/dashboard/Data/Browser/BrowserTable.react.js
+++ b/src/dashboard/Data/Browser/BrowserTable.react.js
@@ -117,7 +117,66 @@ export default class BrowserTable extends React.Component {
(rowWidth, { visible, width }) => visible ? rowWidth + width : rowWidth,
this.props.onAddRow ? 210 : 0
);
- let newRow = null;
+ let editCloneRows;
+ if(this.props.editCloneRows){
+ editCloneRows = (
+
+ {this.props.editCloneRows.map((cloneRow, idx) => {
+ let index = (this.props.editCloneRows.length + 1) * -1 + idx;
+ const currentCol = this.props.current && this.props.current.row === index ? this.props.current.col : undefined;
+ const isEditingRow = this.props.current && this.props.current.row === index && !!this.props.editing;
+ return (
+
+
+
+ );
+ })}
+
+ )
+ }
+ let newRow;
if (this.props.newObject && this.state.offset <= 0) {
const currentCol = this.props.current && this.props.current.row === -1 ? this.props.current.col : undefined;
newRow = (
@@ -143,8 +202,7 @@ export default class BrowserTable extends React.Component {
setCopyableValue={this.props.setCopyableValue}
setContextMenu={this.props.setContextMenu}
onEditSelectedRow={this.props.onEditSelectedRow}
- markRequiredField={this.props.markRequiredField}
- requiredColumnFields={this.props.requiredColumnFields}
+ markRequiredFieldRow={this.props.markRequiredFieldRow}
/>
-1 && this.props.newObject) {
+ //for data rows when there's new row
wrapTop += 60;
}
+ if (this.props.current.row >= -1 && this.props.editCloneRows) {
+ //for data rows & new row when there are edit clone rows
+ wrapTop += (2 * ROW_HEIGHT) * (this.props.editCloneRows.length);
+ }
let wrapLeft = 30;
for (let i = 0; i < this.props.current.col; i++) {
const column = this.props.order[i];
@@ -318,6 +388,7 @@ export default class BrowserTable extends React.Component {
table = (
+ {editCloneRows}
{newRow}
{rows}
diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js
index 1a738ea7fa..96698e5d49 100644
--- a/src/dashboard/Data/Browser/BrowserToolbar.react.js
+++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js
@@ -15,7 +15,7 @@ import Separator from 'components/BrowserMenu/Separator.react';
import styles from 'dashboard/Data/Browser/Browser.scss';
import Toolbar from 'components/Toolbar/Toolbar.react';
import SecurityDialog from 'dashboard/Data/Browser/SecurityDialog.react';
-import ColumnsConfiguration from 'components/ColumnsConfiguration/ColumnsConfiguration.react';
+import ColumnsConfiguration from 'components/ColumnsConfiguration/ColumnsConfiguration.react'
import SecureFieldsDialog from 'dashboard/Data/Browser/SecureFieldsDialog.react';
import LoginDialog from 'dashboard/Data/Browser/LoginDialog.react';
import Toggle from 'components/Toggle/Toggle.react';
@@ -51,6 +51,8 @@ let BrowserToolbar = ({
uniqueField,
handleColumnDragDrop,
handleColumnsOrder,
+ editCloneRows,
+ onCancelPendingEditRows,
order,
enableDeleteAllRows,
@@ -67,6 +69,7 @@ let BrowserToolbar = ({
toggleMasterKeyUsage,
}) => {
let selectionLength = Object.keys(selection).length;
+ let isPendingEditCloneRows = editCloneRows && editCloneRows.length > 0;
let details = [];
if (count !== undefined) {
if (count === 1) {
@@ -111,7 +114,7 @@ let BrowserToolbar = ({
);
} else if (onAddRow) {
menu = (
-
+
{enableColumnManipulation ? : }
@@ -160,6 +163,10 @@ let BrowserToolbar = ({
classes.push(styles.toolbarButtonDisabled);
onClick = null;
}
+ if (isPendingEditCloneRows) {
+ classes.push(styles.toolbarButtonDisabled);
+ onClick = null;
+ }
const columns = {};
const userPointers = [];
@@ -210,6 +217,7 @@ let BrowserToolbar = ({
handleColumnsOrder={handleColumnsOrder}
handleColumnDragDrop={handleColumnDragDrop}
order={order}
+ disabled={isPendingEditCloneRows}
/>
{onAddRow && (
@@ -226,6 +234,7 @@ let BrowserToolbar = ({
title={currentUser ? 'Browsing' : 'Browse'}
icon="users-solid"
active={!!currentUser}
+ disabled={isPendingEditCloneRows}
>
{currentUser ?
)}
{onAddRow && }
-
+
Refresh
@@ -245,6 +254,7 @@ let BrowserToolbar = ({
onChange={onFilterChange}
className={classNameForEditors}
blacklistedFilters={onAddRow ? [] : ['unique']}
+ disabled={isPendingEditCloneRows}
/>
{onAddRow && }
{perms && enableSecurityDialog ? (
@@ -280,7 +290,7 @@ let BrowserToolbar = ({
setCurrent={setCurrent}
title="Security"
icon="locked-solid"
- disabled={!!relation || !!isUnique}
+ disabled={!!relation || !!isUnique || isPendingEditCloneRows}
>
@@ -294,6 +304,16 @@ let BrowserToolbar = ({
)}
{menu}
+ {editCloneRows && editCloneRows.length > 0 && }
+ {editCloneRows && editCloneRows.length > 0 && (
+
+
+
+ )}
+
);
};
diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js
index fb8431d23f..bafa3b1c13 100644
--- a/src/dashboard/Data/Browser/DataBrowser.react.js
+++ b/src/dashboard/Data/Browser/DataBrowser.react.js
@@ -269,7 +269,7 @@ export default class DataBrowser extends React.Component {
}
render() {
- let { className, count, disableSecurityDialog, ...other } = this.props;
+ let { className, count, disableSecurityDialog, onCancelPendingEditRows, editCloneRows, ...other } = this.props;
const { preventSchemaEdits } = this.context.currentApp;
return (
@@ -279,6 +279,7 @@ export default class DataBrowser extends React.Component {
editing={this.state.editing}
simplifiedSchema={this.state.simplifiedSchema}
className={className}
+ editCloneRows={editCloneRows}
handleHeaderDragDrop={this.handleHeaderDragDrop}
handleResize={this.handleResize}
setEditing={this.setEditing}
@@ -300,6 +301,8 @@ export default class DataBrowser extends React.Component {
enableClassManipulation={!preventSchemaEdits}
handleColumnDragDrop={this.handleHeaderDragDrop}
handleColumnsOrder={this.handleColumnsOrder}
+ editCloneRows={editCloneRows}
+ onCancelPendingEditRows={onCancelPendingEditRows}
order={this.state.order}
{...other} />
diff --git a/src/icons/clone-icon-license.txt b/src/icons/clone-icon-license.txt
new file mode 100644
index 0000000000..4098688b8c
--- /dev/null
+++ b/src/icons/clone-icon-license.txt
@@ -0,0 +1 @@
+"clone-icon.svg" by FontAwesome (fontawesome.com) is licensed under CC BY 4.0.
\ No newline at end of file
diff --git a/src/icons/clone-icon.svg b/src/icons/clone-icon.svg
new file mode 100644
index 0000000000..6a05d8375d
--- /dev/null
+++ b/src/icons/clone-icon.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file