diff --git a/package-lock.json b/package-lock.json index b92edf9d84..74315bf62e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -119,6 +119,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -1094,6 +1100,14 @@ "@babel/helper-plugin-utils": "^7.0.0", "resolve": "^1.8.1", "semver": "^5.5.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "@babel/plugin-transform-shorthand-properties": { @@ -1232,6 +1246,12 @@ "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -4074,6 +4094,14 @@ "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "cryptiles": { @@ -4641,6 +4669,13 @@ "lru-cache": "^4.1.5", "semver": "^5.6.0", "sigmund": "^1.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } } }, "ee-first": { @@ -8143,6 +8178,14 @@ "natural-compare": "^1.4.0", "pretty-format": "^24.8.0", "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "jest-util": { @@ -8410,6 +8453,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -8802,6 +8851,12 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -9579,6 +9634,14 @@ "semver": "^5.5.0", "shellwords": "^0.1.1", "which": "^1.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "node-pre-gyp": { @@ -9607,6 +9670,13 @@ "dev": true, "optional": true }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true, + "optional": true + }, "tar": { "version": "4.4.10", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", @@ -9639,6 +9709,14 @@ "dev": true, "requires": { "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "node-rsa": { @@ -9827,6 +9905,14 @@ "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "normalize-path": { @@ -10362,10 +10448,10 @@ "requires": { "@apollographql/graphql-playground-html": "1.6.24", "@parse/fs-files-adapter": "1.0.1", - "@parse/push-adapter": "3.0.0", + "@parse/push-adapter": "3.0.8", "@parse/s3-files-adapter": "1.2.3", "@parse/simple-mailgun-adapter": "1.1.0", - "apollo-server-express": "2.7.0", + "apollo-server-express": "2.8.0", "bcrypt": "3.0.6", "bcryptjs": "2.4.3", "body-parser": "1.19.0", @@ -10385,14 +10471,13 @@ "mime": "2.4.4", "mongodb": "3.2.7", "node-rsa": "1.0.5", - "parse": "2.5.1", - "pg-promise": "8.7.5", + "parse": "2.6.0", + "pg-promise": "9.0.0", "redis": "2.8.0", "semver": "6.3.0", "subscriptions-transport-ws": "0.9.16", "tv4": "1.3.0", "uuid": "3.3.2", - "uws": "10.148.1", "winston": "3.2.1", "winston-daily-rotate-file": "3.10.0", "ws": "7.1.1" @@ -12113,6 +12198,12 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", "dev": true + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -12572,6 +12663,14 @@ "neo-async": "^2.5.0", "pify": "^3.0.0", "semver": "^5.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true + } } }, "sax": { @@ -12633,9 +12732,9 @@ "optional": true }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "send": { "version": "0.17.1", @@ -14063,13 +14162,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, - "uws": { - "version": "10.148.1", - "resolved": "https://registry.npmjs.org/uws/-/uws-10.148.1.tgz", - "integrity": "sha1-/Rp5z2EYo4jgob7YoTlwMNLE/Sw=", - "dev": true, - "optional": true - }, "v8-compile-cache": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", diff --git a/package.json b/package.json index e72b9f74cb..efef36766b 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "react-helmet": "5.2.1", "react-redux": "5.1.1", "react-router": "5.0.1", - "react-router-dom": "5.0.1" + "react-router-dom": "5.0.1", + "semver": "^6.3.0" }, "devDependencies": { "@babel/core": "7.5.5", diff --git a/src/components/Toggle/Toggle.react.js b/src/components/Toggle/Toggle.react.js index 03837ffd08..5cb9d07f66 100644 --- a/src/components/Toggle/Toggle.react.js +++ b/src/components/Toggle/Toggle.react.js @@ -100,7 +100,7 @@ export default class Toggle extends React.Component { toggleClasses.push(styles.darkBg); } return ( -
+
{labelLeft} {labelRight} @@ -122,6 +122,7 @@ Toggle.propTypes = { labelRight: PropTypes.string.describe('Custom right toggle label, case when label does not equal content. [For Toggle.Type.CUSTOM]'), colored: PropTypes.bool.describe('Flag describing is toggle is colored. [For Toggle.Type.CUSTOM]'), darkBg: PropTypes.bool, + additionalStyles: PropTypes.object.describe('Additional styles for Toggle component.'), }; Toggle.Types = { diff --git a/src/dashboard/Data/Browser/AddColumnDialog.react.js b/src/dashboard/Data/Browser/AddColumnDialog.react.js index 335c1d1531..597e72798f 100644 --- a/src/dashboard/Data/Browser/AddColumnDialog.react.js +++ b/src/dashboard/Data/Browser/AddColumnDialog.react.js @@ -5,13 +5,22 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ + +import Parse from 'parse' +import React from 'react'; +import semver from 'semver'; import Dropdown from 'components/Dropdown/Dropdown.react'; import Field from 'components/Field/Field.react'; import Label from 'components/Label/Label.react'; import Modal from 'components/Modal/Modal.react'; import Option from 'components/Dropdown/Option.react'; -import React from 'react'; import TextInput from 'components/TextInput/TextInput.react'; +import Toggle from 'components/Toggle/Toggle.react'; +import DateTimeInput from 'components/DateTimeInput/DateTimeInput.react'; +import SegmentSelect from 'components/SegmentSelect/SegmentSelect.react'; +import FileInput from 'components/FileInput/FileInput.react'; +import styles from 'dashboard/Data/Browser/Browser.scss'; +import validateNumeric from 'lib/validateNumeric'; import { DataTypes, SpecialClasses @@ -27,21 +36,50 @@ export default class AddColumnDialog extends React.Component { this.state = { type: 'String', target: props.classes[0], - name: '' + name: '', + required: false, + defaultValue: undefined, + isDefaultValueValid: true }; + this.renderDefaultValueInput = this.renderDefaultValueInput.bind(this) + this.handleDefaultValueChange = this.handleDefaultValueChange.bind(this) } valid() { - if (this.state.name.length === 0) { - return false; - } - if (!validColumnName(this.state.name)) { - return false; - } - if (this.props.currentColumns.indexOf(this.state.name) > -1) { - return false; + const { name, isDefaultValueValid } = this.state + + return ( + name && + name.length > 0 && + validColumnName(this.state.name) && + this.props.currentColumns.indexOf(this.state.name) === -1 && + isDefaultValueValid + ) + } + + async handlePointer(objectId, target) { + const targetClass = new Parse.Object.extend(target) + const query = new Parse.Query(targetClass) + const result = await query.get(objectId) + return result.toPointer() + } + + getBase64(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = error => reject(error); + }); + } + + async handleFile(file) { + if (file) { + let base64 = await this.getBase64(file); + const parseFile = new Parse.File(file.name, { base64 }); + await parseFile.save(); + return parseFile } - return true; } renderClassDropdown() { @@ -54,11 +92,92 @@ export default class AddColumnDialog extends React.Component { ); } + async handleDefaultValueChange(defaultValue) { + const { type, target } = this.state + let formattedValue = undefined + let isDefaultValueValid = true + + try { + switch (type) { + case 'String': + formattedValue = defaultValue.toString() + break + case 'Number': + if (!validateNumeric(defaultValue)) throw 'Invalid number' + formattedValue = +defaultValue + break + case 'Array': + if (!Array.isArray(JSON.parse(defaultValue))) throw 'Invalid array' + formattedValue = JSON.parse(defaultValue) + break + case 'Object': + if (typeof JSON.parse(defaultValue) !== 'object' || Array.isArray(JSON.parse(defaultValue))) throw 'Invalid object' + formattedValue = JSON.parse(defaultValue) + break + case 'Date': + formattedValue = { __type: 'Date', iso: new Date(defaultValue) } + break + case 'Polygon': + formattedValue = new Parse.Polygon(JSON.parse(defaultValue)) + break + case 'GeoPoint': + formattedValue = new Parse.GeoPoint(JSON.parse(defaultValue)) + break; + case 'Pointer': + formattedValue = await this.handlePointer(defaultValue, target) + break + case 'Boolean': + formattedValue = (defaultValue === 'True' ? true : (defaultValue === 'False' ? false : undefined)) + break + case 'File': + formattedValue = await this.handleFile(defaultValue) + break + } + } catch (e) { + isDefaultValueValid = defaultValue === '' + } + return await this.setState({ defaultValue: formattedValue, isDefaultValueValid }) + } + + renderDefaultValueInput() { + const { type } = this.state + switch (type) { + case 'Array': + case 'Object': + case 'Polygon': + case 'GeoPoint': + return await this.handleDefaultValueChange(defaultValue)} /> + case 'Number': + case 'String': + case 'Pointer': + return await this.handleDefaultValueChange(defaultValue)} /> + case 'Date': + return await this.handleDefaultValueChange(defaultValue)} /> + case 'Boolean': + return await this.handleDefaultValueChange(defaultValue)} /> + case 'File': + return await this.handleDefaultValueChange(defaultValue)} /> + } + } + + render() { let typeDropdown = ( this.setState({ type: type })}> + onChange={(type) => this.setState({ type: type, defaultValue: undefined, required: false })}> {DataTypes.map((t) => )} ); @@ -74,13 +193,13 @@ export default class AddColumnDialog extends React.Component { cancelText={'Never mind, don\u2019t.'} onCancel={this.props.onCancel} onConfirm={() => { - this.props.onConfirm(this.state.type, this.state.name, this.state.target); + this.props.onConfirm(this.state); }}> - } + } input={typeDropdown} /> {this.state.type === 'Pointer' || this.state.type === 'Relation' ? : null} } - input={ this.setState({ name })} />}/> + input={ this.setState({ name })} />} /> + { + /* + Allow include require fields and default values if the parse-server + version is greater than or equal 3.7.0, that is the minimum version + support this feature and check if the field is not a relation + */ + semver.valid(this.props.parseServerVersion) && + semver.gte(this.props.parseServerVersion, '3.7.0') && + this.state.type !== 'Relation' ? + <> + } + input={this.renderDefaultValueInput()} + className={styles.addColumnToggleWrapper} /> + } + input={ this.setState({ required })} additionalStyles={{ margin: '0px' }} />} + className={styles.addColumnToggleWrapper} /> + + : null + } ); } diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index fbab9aa92d..b41a7b2739 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -258,12 +258,14 @@ class Browser extends DashboardView { }); } - addColumn(type, name, target) { + addColumn({ type, name, target, required, defaultValue }) { let payload = { className: this.props.params.className, columnType: type, name: name, - targetClass: target + targetClass: target, + required, + defaultValue }; this.props.schema.dispatch(ActionTypes.ADD_COLUMN, payload).finally(() => { this.setState({ showAddColumnDialog: false }); @@ -991,6 +993,7 @@ class Browser extends DashboardView { onConfirm={this.createClass} /> ); } else if (this.state.showAddColumnDialog) { + const { currentApp = {} } = this.context; let currentColumns = []; classes.get(className).forEach((field, name) => { currentColumns.push(name); @@ -1000,7 +1003,8 @@ class Browser extends DashboardView { currentColumns={currentColumns} classes={this.props.schema.data.get('classes').keySeq().toArray()} onCancel={() => this.setState({ showAddColumnDialog: false })} - onConfirm={this.addColumn} /> + onConfirm={this.addColumn} + parseServerVersion={currentApp.serverInfo && currentApp.serverInfo.parseServerVersion} /> ); } else if (this.state.showRemoveColumnDialog) { let currentColumns = this.getClassColumns(className).map(column => column.name); diff --git a/src/dashboard/Data/Browser/Browser.scss b/src/dashboard/Data/Browser/Browser.scss index f7c9e391b8..7e389ed62e 100644 --- a/src/dashboard/Data/Browser/Browser.scss +++ b/src/dashboard/Data/Browser/Browser.scss @@ -138,6 +138,16 @@ } } +.addColumnToggleWrapper { + >:nth-child(2) { + display: flex; + align-items: center; + justify-content: center; + width: 50%; + background: #f6fafb; + } +} + .notificationMessage, .notificationError { @include animation('fade-in 0.2s ease-out'); position: absolute; diff --git a/src/lib/stores/SchemaStore.js b/src/lib/stores/SchemaStore.js index 17b8fd9c07..6e433fcfe7 100644 --- a/src/lib/stores/SchemaStore.js +++ b/src/lib/stores/SchemaStore.js @@ -74,7 +74,9 @@ function SchemaStore(state, action) { case ActionTypes.ADD_COLUMN: let newField = { [action.name]: { - type: action.columnType + type: action.columnType, + required: action.required, + defaultValue: action.defaultValue } }; if (action.columnType === 'Pointer' || action.columnType === 'Relation') { diff --git a/src/parse-interface-guide/routes.js b/src/parse-interface-guide/routes.js index fd40c3d036..c270bb8f64 100644 --- a/src/parse-interface-guide/routes.js +++ b/src/parse-interface-guide/routes.js @@ -11,7 +11,7 @@ import { Router, Route } from 'react-router'; import { createBrowserHistory } from 'history'; const history = createBrowserHistory({}); -module.exports = ( +const routes = (
{ @@ -20,3 +20,5 @@ module.exports = (
); + +export default routes