diff --git a/src/components/BrowserCell/BrowserCell.react.js b/src/components/BrowserCell/BrowserCell.react.js
index 2001ea1021..bfb6d9801a 100644
--- a/src/components/BrowserCell/BrowserCell.react.js
+++ b/src/components/BrowserCell/BrowserCell.react.js
@@ -9,16 +9,20 @@ import { dateStringUTC } from 'lib/DateUtils';
import getFileName from 'lib/getFileName';
import Parse from 'parse';
import Pill from 'components/Pill/Pill.react';
-import React, { useEffect, useRef }
- from 'react';
+import React, { Component } from 'react';
import styles from 'components/BrowserCell/BrowserCell.scss';
import { unselectable } from 'stylesheets/base.scss';
-let BrowserCell = ({ type, value, hidden, width, current, onSelect, onEditChange, setRelation, onPointerClick }) => {
- const cellRef = current ? useRef() : null;
- if (current) {
- useEffect(() => {
- const node = cellRef.current;
+export default class BrowserCell extends Component {
+ constructor() {
+ super();
+
+ this.cellRef = React.createRef();
+ }
+
+ componentDidUpdate() {
+ if (this.props.current) {
+ const node = this.cellRef.current;
const { left, right, bottom, top } = node.getBoundingClientRect();
// Takes into consideration Sidebar width when over 980px wide.
@@ -28,118 +32,137 @@ let BrowserCell = ({ type, value, hidden, width, current, onSelect, onEditChange
const topBoundary = 126;
if (left < leftBoundary || right > window.innerWidth) {
- node.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
+ node.scrollIntoView({ block: 'nearest', inline: 'start' });
} else if (top < topBoundary || bottom > window.innerHeight) {
- node.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
+ node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
- });
+ }
}
- let content = value;
- let classes = [styles.cell, unselectable];
- if (hidden) {
- content = '(hidden)';
- classes.push(styles.empty);
- } else if (value === undefined) {
- if (type === 'ACL') {
- content = 'Public Read + Write';
- } else {
- content = '(undefined)';
- classes.push(styles.empty);
- }
- } else if (value === null) {
- content = '(null)';
- classes.push(styles.empty);
- } else if (value === '') {
- content = ;
- classes.push(styles.empty);
- } else if (type === 'Pointer') {
- if (value && value.__type) {
- const object = new Parse.Object(value.className);
- object.id = value.objectId;
- value = object;
+ shouldComponentUpdate(nextProps) {
+ const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
+ .filter(propName => propName !== 'value');
+ if (shallowVerifyProps.some(propName => this.props[propName] !== nextProps[propName])) {
+ return true;
}
- content = (
-
-
-
- );
- } else if (type === 'Date') {
- if (typeof value === 'object' && value.__type) {
- value = new Date(value.iso);
- } else if (typeof value === 'string') {
- value = new Date(value);
+ const { value } = this.props;
+ const { value: nextValue } = nextProps;
+ if (typeof value !== typeof nextValue) {
+ return true;
}
- content = dateStringUTC(value);
- } else if (type === 'Boolean') {
- content = value ? 'True' : 'False';
- } else if (type === 'Object' || type === 'Bytes' || type === 'Array') {
- content = JSON.stringify(value);
- } else if (type === 'File') {
- if (value.url()) {
- content = ;
- } else {
- content = ;
+ const isRefDifferent = value !== nextValue;
+ if (isRefDifferent && typeof value === 'object') {
+ return JSON.stringify(value) !== JSON.stringify(nextValue);
}
- } else if (type === 'ACL') {
- let pieces = [];
- let json = value.toJSON();
- if (Object.prototype.hasOwnProperty.call(json, '*')) {
- if (json['*'].read && json['*'].write) {
- pieces.push('Public Read + Write');
- } else if (json['*'].read) {
- pieces.push('Public Read');
- } else if (json['*'].write) {
- pieces.push('Public Write');
+ return isRefDifferent;
+ }
+
+ render() {
+ let { type, value, hidden, width, current, onSelect, onEditChange, setRelation, onPointerClick, row, col } = this.props;
+ let content = value;
+ let classes = [styles.cell, unselectable];
+ if (hidden) {
+ content = '(hidden)';
+ classes.push(styles.empty);
+ } else if (value === undefined) {
+ if (type === 'ACL') {
+ content = 'Public Read + Write';
+ } else {
+ content = '(undefined)';
+ classes.push(styles.empty);
}
- }
- for (let role in json) {
- if (role !== '*') {
- pieces.push(role);
+ } else if (value === null) {
+ content = '(null)';
+ classes.push(styles.empty);
+ } else if (value === '') {
+ content = ;
+ classes.push(styles.empty);
+ } else if (type === 'Pointer') {
+ if (value && value.__type) {
+ const object = new Parse.Object(value.className);
+ object.id = value.objectId;
+ value = object;
+ }
+ content = (
+
+
+
+ );
+ } else if (type === 'Date') {
+ if (typeof value === 'object' && value.__type) {
+ value = new Date(value.iso);
+ } else if (typeof value === 'string') {
+ value = new Date(value);
}
+ content = dateStringUTC(value);
+ } else if (type === 'Boolean') {
+ content = value ? 'True' : 'False';
+ } else if (type === 'Object' || type === 'Bytes' || type === 'Array') {
+ content = JSON.stringify(value);
+ } else if (type === 'File') {
+ if (value.url()) {
+ content = ;
+ } else {
+ content = ;
+ }
+ } else if (type === 'ACL') {
+ let pieces = [];
+ let json = value.toJSON();
+ if (Object.prototype.hasOwnProperty.call(json, '*')) {
+ if (json['*'].read && json['*'].write) {
+ pieces.push('Public Read + Write');
+ } else if (json['*'].read) {
+ pieces.push('Public Read');
+ } else if (json['*'].write) {
+ pieces.push('Public Write');
+ }
+ }
+ for (let role in json) {
+ if (role !== '*') {
+ pieces.push(role);
+ }
+ }
+ if (pieces.length === 0) {
+ pieces.push('Master Key Only');
+ }
+ content = pieces.join(', ');
+ } else if (type === 'GeoPoint') {
+ content = `(${value.latitude}, ${value.longitude})`;
+ } else if (type === 'Polygon') {
+ content = value.coordinates.map(coord => `(${coord})`)
+ } else if (type === 'Relation') {
+ content = (
+
+
setRelation(value)} value='View relation' />
+
+ );
}
- if (pieces.length === 0) {
- pieces.push('Master Key Only');
+
+ if (current) {
+ classes.push(styles.current);
}
- content = pieces.join(', ');
- } else if (type === 'GeoPoint') {
- content = `(${value.latitude}, ${value.longitude})`;
- } else if (type === 'Polygon') {
- content = value.coordinates.map(coord => `(${coord})`)
- } else if (type === 'Relation') {
- content = (
-
-
setRelation(value)} value='View relation' />
-
+ return (
+ onSelect({ row, col })}
+ onDoubleClick={() => {
+ if (type !== 'Relation') {
+ onEditChange(true)
+ }
+ }}
+ onTouchEnd={e => {
+ if (current && type !== 'Relation') {
+ // The touch event may trigger an unwanted change in the column value
+ if (['ACL', 'Boolean', 'File'].includes(type)) {
+ e.preventDefault();
+ }
+ onEditChange(true);
+ }
+ }}>
+ {content}
+
);
}
-
- if (current) {
- classes.push(styles.current);
- }
- return (
- {
- if (type !== 'Relation') {
- onEditChange(true)
- }
- }}
- onTouchEnd={e => {
- if (current && type !== 'Relation') {
- // The touch event may trigger an unwanted change in the column value
- if (['ACL', 'Boolean', 'File'].includes(type)) {
- e.preventDefault();
- }
- onEditChange(true);
- }
- }}>
- {content}
-
- );
-};
-
-export default BrowserCell;
+}
diff --git a/src/components/BrowserRow/BrowserRow.react.js b/src/components/BrowserRow/BrowserRow.react.js
new file mode 100644
index 0000000000..ad15e697d9
--- /dev/null
+++ b/src/components/BrowserRow/BrowserRow.react.js
@@ -0,0 +1,80 @@
+import Parse from 'parse';
+import React, { Component } from 'react';
+
+import BrowserCell from 'components/BrowserCell/BrowserCell.react';
+import styles from 'dashboard/Data/Browser/Browser.scss';
+
+export default class BrowserRow extends Component {
+ shouldComponentUpdate(nextProps) {
+ const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
+ .filter(propName => propName !== 'obj');
+ if (shallowVerifyProps.some(propName => this.props[propName] !== nextProps[propName])) {
+ return true;
+ }
+ const { obj } = this.props;
+ const { obj: nextObj } = nextProps;
+ const isRefDifferent = obj !== nextObj;
+ return isRefDifferent ? JSON.stringify(obj) !== JSON.stringify(nextObj) : isRefDifferent;
+ }
+
+ render() {
+ const { className, columns, currentCol, isUnique, obj, onPointerClick, order, readOnlyFields, row, rowWidth, selection, selectRow, setCurrent, setEditing, setRelation } = this.props;
+ let attributes = obj.attributes;
+ return (
+
+
+ selectRow(obj.id, e.target.checked)} />
+
+ {order.map(({ name, width, visible }, j) => {
+ if (!visible) return null;
+ let type = columns[name].type;
+ let attr = obj;
+ if (!isUnique) {
+ attr = attributes[name];
+ if (name === 'objectId') {
+ attr = obj.id;
+ } else if (name === 'ACL' && className === '_User' && !attr) {
+ attr = new Parse.ACL({ '*': { read: true }, [obj.id]: { read: true, write: true }});
+ } else if (type === 'Relation' && !attr && obj.id) {
+ attr = new Parse.Relation(obj, name);
+ attr.targetClassName = columns[name].targetClass;
+ } else if (type === 'Array' || type === 'Object') {
+ // This is needed to avoid unwanted conversions of objects to Parse.Objects.
+ // "Parse._encoding" is responsible to convert Parse data into raw data.
+ // Since array and object are generic types, we want to render them the way
+ // they were stored in the database.
+ attr = Parse._encode(obj.get(name));
+ }
+ }
+ let hidden = false;
+ if (name === 'password' && className === '_User') {
+ hidden = true;
+ } else if (name === 'sessionToken') {
+ if (className === '_User' || className === '_Session') {
+ hidden = true;
+ }
+ }
+ return (
+ -1}
+ width={width}
+ current={currentCol === j}
+ onSelect={setCurrent}
+ onEditChange={setEditing}
+ onPointerClick={onPointerClick}
+ setRelation={setRelation}
+ value={attr}
+ hidden={hidden} />
+ );
+ })}
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js
index b4e7bcd284..dcbe2baaec 100644
--- a/src/dashboard/Data/Browser/Browser.react.js
+++ b/src/dashboard/Data/Browser/Browser.react.js
@@ -910,13 +910,6 @@ class Browser extends DashboardView {
);
} else if (className && classes.get(className)) {
- let schema = {};
- classes.get(className).forEach(({ type, targetClass }, col) => {
- schema[col] = {
- type,
- targetClass,
- };
- });
let columns = {
objectId: { type: 'String' }
@@ -924,20 +917,13 @@ class Browser extends DashboardView {
if (this.state.isUnique) {
columns = {};
}
- let userPointers = [];
- classes.get(className).forEach((field, name) => {
- if (name === 'objectId') {
- return;
- }
- if (this.state.isUnique && name !== this.state.uniqueField) {
+ classes.get(className).forEach(({ type, targetClass }, name) => {
+ if (name === 'objectId' || this.state.isUnique && name !== this.state.uniqueField) {
return;
}
- let info = { type: field.type };
- if (field.targetClass) {
- info.targetClass = field.targetClass;
- if (field.targetClass === '_User') {
- userPointers.push(name);
- }
+ const info = { type };
+ if (targetClass) {
+ info.targetClass = targetClass;
}
columns[name] = info;
});
@@ -958,8 +944,7 @@ class Browser extends DashboardView {
uniqueField={this.state.uniqueField}
count={count}
perms={this.state.clp[className]}
- schema={schema}
- userPointers={userPointers}
+ schema={this.props.schema}
filters={this.state.filters}
onFilterChange={this.updateFilters}
onRemoveColumn={this.showRemoveColumn}
diff --git a/src/dashboard/Data/Browser/BrowserTable.react.js b/src/dashboard/Data/Browser/BrowserTable.react.js
index 7c60289361..9ec2a39531 100644
--- a/src/dashboard/Data/Browser/BrowserTable.react.js
+++ b/src/dashboard/Data/Browser/BrowserTable.react.js
@@ -5,7 +5,7 @@
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
-import BrowserCell from 'components/BrowserCell/BrowserCell.react';
+import BrowserRow from 'components/BrowserRow/BrowserRow.react';
import * as browserUtils from 'lib/browserUtils';
import DataBrowserHeaderBar from 'components/DataBrowserHeaderBar/DataBrowserHeaderBar.react';
import Editor from 'dashboard/Data/Browser/Editor.react';
@@ -18,7 +18,8 @@ import Button from 'components/Button/Button.react';
import ParseApp from 'lib/ParseApp';
import PropTypes from 'lib/PropTypes';
-const MAX_ROWS = 60; // Number of rows to render at any time
+const MAX_ROWS = 200; // Number of rows to render at any time
+const ROWS_OFFSET = 160;
const ROW_HEIGHT = 31;
const READ_ONLY = [ 'objectId', 'createdAt', 'updatedAt' ];
@@ -61,83 +62,31 @@ export default class BrowserTable extends React.Component {
return;
}
requestAnimationFrame(() => {
- let rowsAbove = Math.floor(this.refs.table.scrollTop / ROW_HEIGHT);
+ const currentScrollTop = this.refs.table.scrollTop;
+ let rowsAbove = Math.floor(currentScrollTop / ROW_HEIGHT);
let offset = this.state.offset;
- if (rowsAbove - this.state.offset > 20) {
- offset = Math.floor(rowsAbove / 10) * 10 - 10;
- } else if (rowsAbove - this.state.offset < 10) {
- offset = Math.max(0, Math.floor(rowsAbove / 10) * 10 - 30);
+ const currentRow = rowsAbove - this.state.offset;
+
+ // If the scroll is near the beginning or end of the offset,
+ // we need to update the table data with the previous/next offset
+ if (currentRow < 10 || currentRow >= ROWS_OFFSET) {
+ // Rounds the number of rows above
+ rowsAbove = Math.floor(rowsAbove / 10) * 10;
+
+ offset = currentRow < 10
+ ? Math.max(0, rowsAbove - ROWS_OFFSET) // Previous set of rows
+ : rowsAbove - 10; // Next set of rows
}
if (this.state.offset !== offset) {
this.setState({ offset });
- this.refs.table.scrollTop = rowsAbove * ROW_HEIGHT;
+ this.refs.table.scrollTop = currentScrollTop;
}
- if (this.props.maxFetched - offset < 100) {
+ if (this.props.maxFetched - offset <= ROWS_OFFSET * 1.4) {
this.props.fetchNextPage();
}
});
}
- renderRow({ row, obj, rowWidth }) {
- let attributes = obj.attributes;
- let index = row - this.state.offset;
- return (
-
-
- this.props.selectRow(obj.id, e.target.checked)} />
-
- {this.props.order.map(({ name, width, visible }, j) => {
- if (!visible) return null;
- let type = this.props.columns[name].type;
- let attr = obj;
- if (!this.props.isUnique) {
- attr = attributes[name];
- if (name === 'objectId') {
- attr = obj.id;
- } else if (name === 'ACL' && this.props.className === '_User' && !attr) {
- attr = new Parse.ACL({ '*': { read: true }, [obj.id]: { read: true, write: true }});
- } else if (type === 'Relation' && !attr && obj.id) {
- attr = new Parse.Relation(obj, name);
- attr.targetClassName = this.props.columns[name].targetClass;
- } else if (type === 'Array' || type === 'Object') {
- // This is needed to avoid unwanted conversions of objects to Parse.Objects.
- // "Parse._encoding" is responsible to convert Parse data into raw data.
- // Since array and object are generic types, we want to render them the way
- // they were stored in the database.
- attr = Parse._encode(obj.get(name));
- }
- }
- let current = this.props.current && this.props.current.row === row && this.props.current.col === j;
- let hidden = false;
- if (name === 'password' && this.props.className === '_User') {
- hidden = true;
- } else if (name === 'sessionToken') {
- if (this.props.className === '_User' || this.props.className === '_Session') {
- hidden = true;
- }
- }
- return (
- -1}
- width={width}
- current={current}
- onSelect={() => this.props.setCurrent({ row: row, col: j })}
- onEditChange={(state) => this.props.setEditing(state)}
- onPointerClick={this.props.onPointerClick}
- setRelation={this.props.setRelation}
- value={attr}
- hidden={hidden} />
- );
- })}
-
- );
- }
-
render() {
let ordering = {};
if (this.props.ordering) {
@@ -167,9 +116,26 @@ export default class BrowserTable extends React.Component {
);
let newRow = null;
if (this.props.newObject && this.state.offset <= 0) {
+ const currentCol = this.props.current && this.props.current.row === -1 ? this.props.current.col : undefined;
newRow = (
- {this.renderRow({ row: -1, obj: this.props.newObject, json: {}, rowWidth: rowWidth })}
+
);
}
@@ -178,7 +144,24 @@ export default class BrowserTable extends React.Component {
for (let i = this.state.offset; i < end; i++) {
let index = i - this.state.offset;
let obj = this.props.data[i];
- rows[index] = this.renderRow({ row: i, obj, rowWidth: rowWidth });
+ const currentCol = this.props.current && this.props.current.row === i ? this.props.current.col : undefined;
+ rows[index] =
}
if (this.props.editing) {
diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js
index 8c2bc1e2f7..29f9682a0c 100644
--- a/src/dashboard/Data/Browser/BrowserToolbar.react.js
+++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js
@@ -24,7 +24,6 @@ let BrowserToolbar = ({
count,
perms,
schema,
- userPointers,
filters,
selection,
relation,
@@ -44,6 +43,7 @@ let BrowserToolbar = ({
onRefresh,
hidePerms,
isUnique,
+ uniqueField,
handleColumnDragDrop,
handleColumnsOrder,
order,
@@ -142,6 +142,25 @@ let BrowserToolbar = ({
classes.push(styles.toolbarButtonDisabled);
onClick = null;
}
+
+ const userPointers = [];
+ const schemaSimplifiedData = {};
+ const classSchema = schema.data.get('classes').get(className);
+ if (classSchema) {
+ classSchema.forEach(({ type, targetClass }, col) => {
+ if (name === 'objectId' || isUnique && name !== uniqueField) {
+ return;
+ }
+ if (targetClass === '_User') {
+ userPointers.push(name);
+ }
+ schemaSimplifiedData[col] = {
+ type,
+ targetClass,
+ };
+ });
+ }
+
return (
@@ -185,4 +204,4 @@ let BrowserToolbar = ({
);
};
-export default BrowserToolbar;
\ No newline at end of file
+export default BrowserToolbar;
diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js
index c9bacb32a5..b79f9174b3 100644
--- a/src/dashboard/Data/Browser/DataBrowser.react.js
+++ b/src/dashboard/Data/Browser/DataBrowser.react.js
@@ -35,10 +35,32 @@ export default class DataBrowser extends React.Component {
};
this.handleKey = this.handleKey.bind(this);
+ this.handleHeaderDragDrop = this.handleHeaderDragDrop.bind(this);
+ this.handleResize = this.handleResize.bind(this);
+ this.setCurrent = this.setCurrent.bind(this);
+ this.setEditing = this.setEditing.bind(this);
+ this.handleColumnsOrder = this.handleColumnsOrder.bind(this);
this.saveOrderTimeout = null;
}
+ shouldComponentUpdate(nextProps, nextState) {
+ const shallowVerifyStates = [...new Set(Object.keys(this.state).concat(Object.keys(nextState)))]
+ .filter(stateName => stateName !== 'order');
+ if (shallowVerifyStates.some(stateName => this.state[stateName] !== nextState[stateName])) {
+ return true;
+ }
+ if (JSON.stringify(this.state.order) !== JSON.stringify(nextState.order)) {
+ return true;
+ }
+ const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
+ .filter(propName => propName !== 'columns');
+ if (shallowVerifyProps.some(propName => this.props[propName] !== nextProps[propName])) {
+ return true;
+ }
+ return JSON.stringify(this.props.columns) !== JSON.stringify(nextProps.columns);
+ }
+
componentWillReceiveProps(props, context) {
if (props.className !== this.props.className) {
let order = ColumnPreferences.getOrder(
@@ -95,8 +117,8 @@ export default class DataBrowser extends React.Component {
* @param {Number} hoverIndex - index of headerbar moved to left of
*/
handleHeaderDragDrop(dragIndex, hoverIndex) {
- let newOrder = this.state.order;
- let movedIndex = newOrder.splice(dragIndex, 1);
+ const newOrder = [ ...this.state.order ];
+ const movedIndex = newOrder.splice(dragIndex, 1);
newOrder.splice(hoverIndex, 0, movedIndex[0]);
this.setState({ order: newOrder }, () => {
this.updatePreferences(newOrder);
@@ -183,19 +205,19 @@ export default class DataBrowser extends React.Component {
}
setCurrent(current) {
- if (this.state.current !== current) {
- this.setState({ current: current });
+ if (JSON.stringify(this.state.current) !== JSON.stringify(current)) {
+ this.setState({ current });
}
}
handleColumnsOrder(order) {
- this.setState({ order }, () => {
+ this.setState({ order: [ ...order ] }, () => {
this.updatePreferences(order);
});
}
render() {
- let { className, ...other } = this.props;
+ let { className, count, ...other } = this.props;
const { preventSchemaEdits } = this.context.currentApp;
return (
@@ -204,23 +226,24 @@ export default class DataBrowser extends React.Component {
current={this.state.current}
editing={this.state.editing}
className={className}
- handleHeaderDragDrop={this.handleHeaderDragDrop.bind(this)}
- handleResize={this.handleResize.bind(this)}
- setEditing={this.setEditing.bind(this)}
- setCurrent={this.setCurrent.bind(this)}
+ handleHeaderDragDrop={this.handleHeaderDragDrop}
+ handleResize={this.handleResize}
+ setEditing={this.setEditing}
+ setCurrent={this.setCurrent}
{...other} />