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