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} />