diff --git a/src/components/PushAudienceDialog/InstallationCondition.react.js b/src/components/PushAudienceDialog/InstallationCondition.react.js
index 7c101c9f3e..ec4df42994 100644
--- a/src/components/PushAudienceDialog/InstallationCondition.react.js
+++ b/src/components/PushAudienceDialog/InstallationCondition.react.js
@@ -162,7 +162,6 @@ export default class InstallationCondition extends React.Component {
return (
}
input={input}
/>
diff --git a/src/components/PushAudienceDialog/PushAudienceDialog.react.js b/src/components/PushAudienceDialog/PushAudienceDialog.react.js
index bd13bc2ce5..366d677f94 100644
--- a/src/components/PushAudienceDialog/PushAudienceDialog.react.js
+++ b/src/components/PushAudienceDialog/PushAudienceDialog.react.js
@@ -10,7 +10,7 @@ import * as PushUtils from 'lib/PushUtils';
import * as PushConstants from 'dashboard/Push/PushConstants';
import Button from 'components/Button/Button.react';
import Field from 'components/Field/Field.react';
-import Filter from 'components/Filter/Filter.react';
+import PushAudienceFilter from 'components/PushAudienceFilter/PushAudienceFilter.react';
import FormNote from 'components/FormNote/FormNote.react';
import InstallationCondition from 'components/PushAudienceDialog/InstallationCondition.react';
import Label from 'components/Label/Label.react';
@@ -100,7 +100,16 @@ export default class PushAudienceDialog extends React.Component {
return;
}
const available = Filters.availableFilters(this.props.schema, this.state.filters);
- const field = Object.keys(available)[0];
+
+ const keys = Object.keys(available);
+ if (keys.length === 0) {
+ this.setState({
+ errorMessage: 'No condition available.',
+ });
+ return;
+ }
+
+ const field = keys[0];
this.setState(
({ filters }) => ({
filters: filters.push(new Map({ field: field, constraint: available[field][0] })),
@@ -283,12 +292,15 @@ export default class PushAudienceDialog extends React.Component {
input={platformSelect}
/>
- {
this.setState({ filters }, this.fetchAudienceSize.bind(this));
}}
+ onDeleteRow={() => {
+ this.setState({ errorMessage: undefined });
+ }}
renderRow={props => }
/>
diff --git a/src/components/PushAudienceFilter/PushAudienceFilter.react.js b/src/components/PushAudienceFilter/PushAudienceFilter.react.js
new file mode 100644
index 0000000000..10cf4bf154
--- /dev/null
+++ b/src/components/PushAudienceFilter/PushAudienceFilter.react.js
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2016-present, Parse, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the license found in the LICENSE file in
+ * the root directory of this source tree.
+ */
+import * as Filters from 'lib/Filters';
+import { List, Map } from 'immutable';
+import PropTypes from 'lib/PropTypes';
+import React from 'react';
+import stringCompare from 'lib/stringCompare';
+import { CurrentApp } from 'context/currentApp';
+
+function changeField(schema, filters, index, newField) {
+ const allowedConstraints = Filters.FieldConstraints[schema[newField].type];
+ const current = filters.get(index);
+ const constraint = current.get('constraint');
+ const compare = current.get('compareTo');
+ const defaultCompare = Filters.DefaultComparisons[schema[newField].type];
+ const useExisting = allowedConstraints.includes(constraint);
+ const newFilter = new Map({
+ field: newField,
+ constraint: useExisting ? constraint : Filters.FieldConstraints[schema[newField].type][0],
+ compareTo: useExisting && typeof defaultCompare === typeof compare ? compare : defaultCompare,
+ });
+ return filters.set(index, newFilter);
+}
+
+function changeConstraint(schema, filters, index, newConstraint, prevCompareTo) {
+ const field = filters.get(index).get('field');
+ let compareType = schema[field].type;
+ if (Object.prototype.hasOwnProperty.call(Filters.Constraints[newConstraint], 'field')) {
+ compareType = Filters.Constraints[newConstraint].field;
+ }
+ const newFilter = new Map({
+ field: field,
+ constraint: newConstraint,
+ compareTo: prevCompareTo ?? Filters.DefaultComparisons[compareType],
+ });
+ return filters.set(index, newFilter);
+}
+
+function changeCompareTo(schema, filters, index, type, newCompare) {
+ const newValue = newCompare;
+ return filters.set(index, filters.get(index).set('compareTo', newValue));
+}
+
+function deleteRow(filters, index) {
+ return filters.delete(index);
+}
+
+const PushAudienceFilter = ({
+ schema,
+ filters,
+ renderRow,
+ onChange,
+ onSearch,
+ blacklist,
+ className,
+ onDeleteRow,
+}) => {
+ const currentApp = React.useContext(CurrentApp);
+ blacklist = blacklist || [];
+ const available = Filters.availableFilters(schema, filters);
+ return (
+
+ {filters.toArray().map((filter, i) => {
+ const field = filter.get('field');
+ const constraint = filter.get('constraint');
+ const compareTo = filter.get('compareTo');
+
+ const fields = Object.keys(available).concat([]);
+ if (fields.indexOf(field) < 0) {
+ fields.push(field);
+ }
+
+ // Get the column preference of the current class.
+ const currentColumnPreference = currentApp.columnPreference
+ ? currentApp.columnPreference[className]
+ : null;
+
+ // Check if the preference exists.
+ if (currentColumnPreference) {
+ const fieldsToSortToTop = currentColumnPreference
+ .filter(item => item.filterSortToTop)
+ .map(item => item.name);
+ // Sort the fields.
+ fields.sort((a, b) => {
+ // Only "a" should sorted to the top.
+ if (fieldsToSortToTop.includes(a) && !fieldsToSortToTop.includes(b)) {
+ return -1;
+ }
+ // Only "b" should sorted to the top.
+ if (!fieldsToSortToTop.includes(a) && fieldsToSortToTop.includes(b)) {
+ return 1;
+ }
+ // Both should sorted to the top -> they should be sorted to the same order as in the "fieldsToSortToTop" array.
+ if (fieldsToSortToTop.includes(a) && fieldsToSortToTop.includes(b)) {
+ return fieldsToSortToTop.indexOf(a) - fieldsToSortToTop.indexOf(b);
+ }
+ return stringCompare(a, b);
+ });
+ }
+ // If there's no preference: Use the default sort function.
+ else {
+ fields.sort();
+ }
+
+ const constraints = Filters.FieldConstraints[schema[field].type].filter(
+ c => blacklist.indexOf(c) < 0
+ );
+ let compareType = schema[field].type;
+ if (Object.prototype.hasOwnProperty.call(Filters.Constraints[constraint], 'field')) {
+ compareType = Filters.Constraints[constraint].field;
+ }
+ return renderRow({
+ fields,
+ constraints,
+ compareInfo: {
+ type: compareType,
+ targetClass: schema[field].targetClass,
+ },
+ currentField: field,
+ currentConstraint: constraint,
+ compareTo,
+ key: field + '-' + constraint + '-' + i,
+
+ onChangeField: newField => {
+ onChange(changeField(schema, filters, i, newField));
+ },
+ onChangeConstraint: (newConstraint, prevCompareTo) => {
+ onChange(changeConstraint(schema, filters, i, newConstraint, prevCompareTo));
+ },
+ onChangeCompareTo: newCompare => {
+ onChange(changeCompareTo(schema, filters, i, compareType, newCompare));
+ },
+ onKeyDown: ({ key }) => {
+ if (key === 'Enter') {
+ onSearch();
+ }
+ },
+ onDeleteRow: () => {
+ onDeleteRow?.();
+ onChange(deleteRow(filters, i));
+ },
+ });
+ })}
+
+ );
+};
+
+export default PushAudienceFilter;
+
+PushAudienceFilter.propTypes = {
+ schema: PropTypes.object.isRequired.describe(
+ 'A class schema, mapping field names to their Type strings'
+ ),
+ filters: PropTypes.instanceOf(List).isRequired.describe(
+ 'An array of filter objects. Each filter contains "field", "comparator", and "compareTo" fields.'
+ ),
+ renderRow: PropTypes.func.isRequired.describe('A function for rendering a row of a filter.'),
+};
diff --git a/src/components/SaveButton/SaveButton.react.js b/src/components/SaveButton/SaveButton.react.js
index f590066b80..2c8e76baf6 100644
--- a/src/components/SaveButton/SaveButton.react.js
+++ b/src/components/SaveButton/SaveButton.react.js
@@ -56,7 +56,7 @@ const SaveButton = ({
);
};
-SaveButton.States = keyMirror(['SAVING', 'SUCCEEDED', 'FAILED']);
+SaveButton.States = keyMirror(['SAVING', 'SUCCEEDED', 'FAILED', 'WAITING']);
const { ...forwardedButtonProps } = Button.propTypes;
delete forwardedButtonProps.value;
diff --git a/src/dashboard/Data/CloudCode/CloudCode.react.js b/src/dashboard/Data/CloudCode/CloudCode.react.js
index 1ee52521ea..36f6d8185c 100644
--- a/src/dashboard/Data/CloudCode/CloudCode.react.js
+++ b/src/dashboard/Data/CloudCode/CloudCode.react.js
@@ -6,6 +6,7 @@
* the root directory of this source tree.
*/
import CodeSnippet from 'components/CodeSnippet/CodeSnippet.react';
+import CodeEditor from 'components/CodeEditor/CodeEditor.react';
import DashboardView from 'dashboard/DashboardView.react';
import EmptyState from 'components/EmptyState/EmptyState.react';
import FileTree from 'components/FileTree/FileTree.react';
@@ -14,11 +15,13 @@ import styles from 'dashboard/Data/CloudCode/CloudCode.scss';
import Toolbar from 'components/Toolbar/Toolbar.react';
import generatePath from 'lib/generatePath';
import { withRouter } from 'lib/withRouter';
+import SaveButton from 'components/SaveButton/SaveButton.react';
function getPath(params) {
- return params.splat;
+ return params['*'];
}
+
@withRouter
class CloudCode extends DashboardView {
constructor() {
@@ -29,6 +32,8 @@ class CloudCode extends DashboardView {
this.state = {
files: undefined,
source: undefined,
+ saveState: SaveButton.States.WAITING,
+ saveError: '',
};
}
@@ -37,7 +42,7 @@ class CloudCode extends DashboardView {
}
componentWillReceiveProps(nextProps, nextContext) {
- if (this.context !== nextContext) {
+ if (this.context !== nextContext || getPath(nextProps.params) !== getPath(this.props.params)) {
this.fetchSource(nextContext, getPath(nextProps.params));
}
}
@@ -54,9 +59,13 @@ class CloudCode extends DashboardView {
if (!fileName || release.files[fileName] === undefined) {
// Means we're still in /cloud_code/. Let's redirect to /cloud_code/main.js
- this.props.navigate(generatePath(this.context, 'cloud_code/main.js'), { replace: true });
+
+ this.props.navigate(
+ generatePath(this.context, `cloud_code/${Object.keys(release.files)[0]}`)
+ );
} else {
// Means we can load /cloud_code/
+ this.setState({ source: undefined });
app.getSource(fileName).then(
source => this.setState({ source: source }),
() => this.setState({ source: undefined })
@@ -90,6 +99,24 @@ class CloudCode extends DashboardView {
);
}
+ async getCode() {
+ if (!this.editor) {
+ return;
+ }
+ this.setState({ saveState: SaveButton.States.SAVING });
+ let fileName = getPath(this.props.params);
+ try {
+ await this.context.saveSource(fileName,this.editor.value);
+ this.setState({ saveState: SaveButton.States.SUCCEEDED });
+ setTimeout(()=> {
+ this.setState({ saveState: SaveButton.States.WAITING });
+ },2000);
+ } catch (e) {
+ this.setState({ saveState: SaveButton.States.FAILED });
+ this.setState({ saveError: e.message || e });
+ }
+ }
+
renderContent() {
let toolbar = null;
let content = null;
@@ -113,11 +140,23 @@ class CloudCode extends DashboardView {
if (fileName) {
toolbar = ;
- const source = this.state.files[fileName];
- if (source && source.source) {
+ const source = this.state.source;
+ if (source) {
content = (
-
+ {/*
*/}
+
(this.editor = editor)}
+ fontSize={14}
+ />
+
+ this.getCode(this)}
+ />
+
);
}
diff --git a/src/lib/ParseApp.js b/src/lib/ParseApp.js
index 318c07fe87..1494175eab 100644
--- a/src/lib/ParseApp.js
+++ b/src/lib/ParseApp.js
@@ -154,6 +154,15 @@ export default class ParseApp {
return this.apiRequest('GET', path, {}, { useMasterKey: true });
}
+ /**
+ * Saves source of a Cloud Code hosted file from api.parse.com
+ * fileName - the name of the file to be fetched
+ * data - the text to save to the cloud file
+ */
+ saveSource(fileName, data) {
+ return this.apiRequest('POST', `scripts/${fileName}`, { data }, { useMasterKey: true });
+ }
+
/**
* Fetches source of a Cloud Code hosted file from api.parse.com
* fileName - the name of the file to be fetched