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