Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ export default class InstallationCondition extends React.Component {

return (
<Field
labelWidth={30}
label={<Label text="Installation Condition" description={labelDescription} />}
input={input}
/>
Expand Down
18 changes: 15 additions & 3 deletions src/components/PushAudienceDialog/PushAudienceDialog.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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] })),
Expand Down Expand Up @@ -283,12 +292,15 @@ export default class PushAudienceDialog extends React.Component {
input={platformSelect}
/>
<div className={styles.filter}>
<Filter
<PushAudienceFilter
schema={this.props.schema}
filters={this.state.filters}
onChange={filters => {
this.setState({ filters }, this.fetchAudienceSize.bind(this));
}}
onDeleteRow={() => {
this.setState({ errorMessage: undefined });
}}
renderRow={props => <InstallationCondition {...props} />}
/>
Comment on lines +295 to 305
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Pressing Enter in a row may crash: onSearch isn’t provided to child.

PushAudienceFilter calls onSearch() on Enter without optional chaining. Since this dialog doesn’t pass onSearch, users pressing Enter can hit a TypeError. Pass a no‑op or wire it to refresh the audience size.

Apply:

   <PushAudienceFilter
     schema={this.props.schema}
     filters={this.state.filters}
     onChange={filters => {
-      this.setState({ filters }, this.fetchAudienceSize.bind(this));
+      this.setState({ filters, errorMessage: undefined }, this.fetchAudienceSize.bind(this));
     }}
+    onSearch={this.fetchAudienceSize.bind(this)}
     onDeleteRow={() => {
       this.setState({ errorMessage: undefined });
     }}
     renderRow={props => <InstallationCondition {...props} />}
   />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PushAudienceFilter
schema={this.props.schema}
filters={this.state.filters}
onChange={filters => {
this.setState({ filters }, this.fetchAudienceSize.bind(this));
}}
onDeleteRow={() => {
this.setState({ errorMessage: undefined });
}}
renderRow={props => <InstallationCondition {...props} />}
/>
<PushAudienceFilter
schema={this.props.schema}
filters={this.state.filters}
onChange={filters => {
this.setState({ filters, errorMessage: undefined }, this.fetchAudienceSize.bind(this));
}}
onSearch={this.fetchAudienceSize.bind(this)}
onDeleteRow={() => {
this.setState({ errorMessage: undefined });
}}
renderRow={props => <InstallationCondition {...props} />}
/>
🤖 Prompt for AI Agents
In src/components/PushAudienceDialog/PushAudienceDialog.react.js around lines
295 to 305, PushAudienceFilter invokes onSearch() on Enter but this dialog does
not pass an onSearch prop, causing a TypeError when users press Enter; fix by
passing an onSearch prop (either a no-op function or point it to refresh the
audience size, e.g., call this.fetchAudienceSize) so the child can safely call
onSearch without crashing and ensure binding/context is correct when using the
component's method.

</div>
Expand Down
163 changes: 163 additions & 0 deletions src/components/PushAudienceFilter/PushAudienceFilter.react.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{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;
}
Comment on lines +110 to +116
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against missing schema entries to avoid crashes.

If a saved filter references a removed field, schema[field] is undefined and this will throw.

Apply minimal guard:

-const constraints = Filters.FieldConstraints[schema[field].type].filter(
+if (!schema[field]) {
+  // Field no longer exists; skip rendering this row safely.
+  return null;
+}
+const constraints = Filters.FieldConstraints[schema[field].type].filter(
   c => blacklist.indexOf(c) < 0
);

Optionally, surface a UI hint instead of skipping; happy to provide.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
if (!schema[field]) {
// Field no longer exists; skip rendering this row safely.
return null;
}
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;
}
🤖 Prompt for AI Agents
In src/components/PushAudienceFilter/PushAudienceFilter.react.js around lines
110 to 116, the code assumes schema[field] exists and uses schema[field].type
which will throw if a saved filter references a removed field; add a guard that
checks if schema[field] is defined before accessing .type and bail out (e.g.,
skip this filter, return an empty constraints array, or set a fallback
compareType) and optionally surface a UI hint or console.warn when a field is
missing; ensure both uses of schema[field].type are protected so the component
does not crash.

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();
}
},
Comment on lines +138 to +142
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Enter key handler can throw when onSearch is undefined.

Use optional chaining to make it safe.

Apply:

 onKeyDown: ({ key }) => {
   if (key === 'Enter') {
-    onSearch();
+    onSearch?.();
   }
 },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onKeyDown: ({ key }) => {
if (key === 'Enter') {
onSearch();
}
},
onKeyDown: ({ key }) => {
if (key === 'Enter') {
onSearch?.();
}
},
🤖 Prompt for AI Agents
In src/components/PushAudienceFilter/PushAudienceFilter.react.js around lines
138 to 142, the onKeyDown handler calls onSearch() directly which can throw if
onSearch is undefined; change the call to use optional chaining so it safely
invokes onSearch only when defined (e.g., call onSearch?.()) and keep the
existing Enter key check unchanged to avoid runtime errors.

onDeleteRow: () => {
onDeleteRow?.();
onChange(deleteRow(filters, i));
},
});
})}
</div>
);
};

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.'),
};
2 changes: 1 addition & 1 deletion src/components/SaveButton/SaveButton.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
51 changes: 45 additions & 6 deletions src/dashboard/Data/CloudCode/CloudCode.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* This source code is licensed under the license found in the LICENSE file in
* the root directory of this source tree.
*/
import CodeSnippet from 'components/CodeSnippet/CodeSnippet.react';

Check failure on line 8 in src/dashboard/Data/CloudCode/CloudCode.react.js

View workflow job for this annotation

GitHub Actions / Lint

'CodeSnippet' is defined but never used
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';
Expand All @@ -14,11 +15,13 @@
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() {
Expand All @@ -29,6 +32,8 @@
this.state = {
files: undefined,
source: undefined,
saveState: SaveButton.States.WAITING,
saveError: '',
};
}

Expand All @@ -37,7 +42,7 @@
}

componentWillReceiveProps(nextProps, nextContext) {
if (this.context !== nextContext) {
if (this.context !== nextContext || getPath(nextProps.params) !== getPath(this.props.params)) {
this.fetchSource(nextContext, getPath(nextProps.params));
}
}
Expand All @@ -54,9 +59,13 @@

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/<fileName>
this.setState({ source: undefined });
app.getSource(fileName).then(
source => this.setState({ source: source }),
() => this.setState({ source: undefined })
Expand Down Expand Up @@ -90,6 +99,24 @@
);
}

async getCode() {

Check failure on line 102 in src/dashboard/Data/CloudCode/CloudCode.react.js

View workflow job for this annotation

GitHub Actions / Lint

Trailing spaces not allowed
if (!this.editor) {
return;
}
this.setState({ saveState: SaveButton.States.SAVING });
let fileName = getPath(this.props.params);

Check failure on line 107 in src/dashboard/Data/CloudCode/CloudCode.react.js

View workflow job for this annotation

GitHub Actions / Lint

'fileName' is never reassigned. Use 'const' instead
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 });
}
}
Comment on lines +114 to +118
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Let the save button recover after a failure.

Once saveState is set to FAILED, SaveButton stops wiring onClick, so the user can’t retry the save without reloading the page. Please transition back to WAITING (after surfacing the error) so retries are possible.

You can mirror the success path timing and clear the failure state shortly after logging the error:

     } catch (e) {
-      this.setState({ saveState: SaveButton.States.FAILED });
-      this.setState({ saveError: e.message || e });
+      this.setState({
+        saveState: SaveButton.States.FAILED,
+        saveError: e.message || e,
+      });
+      setTimeout(() => {
+        this.setState({ saveState: SaveButton.States.WAITING });
+      }, 2000);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (e) {
this.setState({ saveState: SaveButton.States.FAILED });
this.setState({ saveError: e.message || e });
}
}
} catch (e) {
this.setState({
saveState: SaveButton.States.FAILED,
saveError: e.message || e,
});
setTimeout(() => {
this.setState({ saveState: SaveButton.States.WAITING });
}, 2000);
}
}
🤖 Prompt for AI Agents
In src/dashboard/Data/CloudCode/CloudCode.react.js around lines 114 to 118, the
catch block sets saveState to FAILED and saveError but never returns the button
to an actionable state; change the catch to set saveState:
SaveButton.States.FAILED and saveError as now, then schedule a follow-up
setState (use the same delay used on the success path) that resets saveState
back to SaveButton.States.WAITING and clears saveError so the SaveButton
re-enables its onClick and the user can retry.


renderContent() {
let toolbar = null;
let content = null;
Expand All @@ -113,11 +140,23 @@
if (fileName) {
toolbar = <Toolbar section="Cloud Code" subsection={fileName} />;

const source = this.state.files[fileName];
if (source && source.source) {
const source = this.state.source;
if (source) {
content = (
<div className={styles.content}>
<CodeSnippet source={source.source} language="javascript" />
{/* <CodeSnippet source={source.source} language="javascript" /> */}
<CodeEditor
defaultValue={source}
ref={editor => (this.editor = editor)}
fontSize={14}
/>
<div style={{ padding: 10, alignContent: 'center', display: 'flex', justifyContent: 'center' }}>
<SaveButton
state={this.state.saveState}
failedText={this.state.saveError}
onClick={() => this.getCode(this)}
/>
</div>
</div>
);
}
Expand Down
9 changes: 9 additions & 0 deletions src/lib/ParseApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
Comment on lines +162 to +164
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the Cloud Code cache in sync after save.

getSource() serves latestRelease.files[fileName].source when it’s already populated, so after a successful saveSource() the dashboard will still hand back the pre-save source until the minute-long cache expires. Please update (or invalidate) the cached entry once the POST resolves so users don’t reopen the file and see stale code despite a green “Saved!”.

Apply this diff to update the cache eagerly:

-  saveSource(fileName, data) {
-    return this.apiRequest('POST', `scripts/${fileName}`, { data }, { useMasterKey: true });
-  }
+  saveSource(fileName, data) {
+    return this.apiRequest('POST', `scripts/${fileName}`, { data }, { useMasterKey: true }).then(
+      result => {
+        if (this.latestRelease.files && this.latestRelease.files[fileName]) {
+          this.latestRelease.files[fileName].source = data;
+        }
+        return result;
+      }
+    );
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
saveSource(fileName, data) {
return this.apiRequest('POST', `scripts/${fileName}`, { data }, { useMasterKey: true });
}
saveSource(fileName, data) {
return this.apiRequest('POST', `scripts/${fileName}`, { data }, { useMasterKey: true })
.then(result => {
if (this.latestRelease.files && this.latestRelease.files[fileName]) {
this.latestRelease.files[fileName].source = data;
}
return result;
});
}
🤖 Prompt for AI Agents
In src/lib/ParseApp.js around lines 162-164, saveSource currently posts the new
script but does not update the in-memory Cloud Code cache, so getSource can
return stale content; change saveSource to update (or invalidate)
latestRelease.files[fileName].source after the POST resolves: call the
apiRequest, then on success check this.latestRelease and
this.latestRelease.files and either set
this.latestRelease.files[fileName].source = data (or delete the cached entry) so
the cache reflects the newly saved content; ensure you only mutate when the POST
succeeds and guard for missing objects.


/**
* Fetches source of a Cloud Code hosted file from api.parse.com
* fileName - the name of the file to be fetched
Expand Down
Loading