Skip to content

Commit 03342d2

Browse files
committed
add file upload text input
1 parent bca20ce commit 03342d2

File tree

5 files changed

+218
-1
lines changed

5 files changed

+218
-1
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,21 @@ Display aggregated data from your classes using a MongoDB aggregation pipeline.
12801280

12811281
Display data returned by a Parse Cloud Function. Create a view specifying a Cloud Function that returns an array of objects. Cloud Functions enable custom business logic, computed fields, and complex data transformations.
12821282

1283+
Cloud Function views can prompt users for text input and/or file upload when opened. Enable "Require text input" or "Require file upload" checkboxes when creating the view. The user provided data will then be available in the Cloud Function as parameters.
1284+
1285+
Cloud Function example:
1286+
1287+
```js
1288+
Parse.Cloud.define("myViewFunction", request => {
1289+
const text = request.params.text;
1290+
const fileData = request.params.fileData;
1291+
return processDataWithTextAndFile(text, fileData);
1292+
});
1293+
```
1294+
1295+
> [!Note]
1296+
> Text and file data are ephemeral and only available to the Cloud Function during that request. Files are base64 encoded, increasing the data during transfer by ~33%.
1297+
12831298
### View Table
12841299

12851300
When designing the aggregation pipeline, consider that some values are rendered specially in the output table.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import Field from 'components/Field/Field.react';
2+
import FileInput from 'components/FileInput/FileInput.react';
3+
import Label from 'components/Label/Label.react';
4+
import Modal from 'components/Modal/Modal.react';
5+
import TextInput from 'components/TextInput/TextInput.react';
6+
import React from 'react';
7+
8+
export default class CloudFunctionInputDialog extends React.Component {
9+
constructor(props) {
10+
super(props);
11+
this.state = {
12+
textInput: '',
13+
uploadedFile: null,
14+
};
15+
}
16+
17+
handleFileChange = (file) => {
18+
this.setState({ uploadedFile: file });
19+
};
20+
21+
handleConfirm = () => {
22+
const { requireTextInput, requireFileUpload } = this.props;
23+
const params = {};
24+
25+
if (requireTextInput) {
26+
params.text = this.state.textInput;
27+
}
28+
29+
if (requireFileUpload && this.state.uploadedFile) {
30+
// For file uploads, we'll pass the raw file data
31+
// The cloud function will receive this as base64 encoded data
32+
const file = this.state.uploadedFile;
33+
const reader = new FileReader();
34+
reader.onload = () => {
35+
if (reader.result && typeof reader.result === 'string') {
36+
params.fileData = {
37+
name: file.name,
38+
type: file.type,
39+
size: file.size,
40+
data: reader.result.split(',')[1], // Remove the data URL prefix
41+
};
42+
}
43+
this.props.onConfirm(params);
44+
};
45+
reader.readAsDataURL(file);
46+
} else {
47+
this.props.onConfirm(params);
48+
}
49+
};
50+
51+
render() {
52+
const { requireTextInput, requireFileUpload, onCancel } = this.props;
53+
54+
// Check if we have all required inputs
55+
const hasRequiredText = !requireTextInput || this.state.textInput.trim().length > 0;
56+
const hasRequiredFile = !requireFileUpload || this.state.uploadedFile !== null;
57+
const isValid = hasRequiredText && hasRequiredFile;
58+
59+
return (
60+
<Modal
61+
type={Modal.Types.INFO}
62+
icon="gear"
63+
iconSize={40}
64+
title="Cloud Function Input"
65+
subtitle="Provide the required input for this view."
66+
confirmText="Send"
67+
cancelText="Cancel"
68+
disabled={!isValid}
69+
onCancel={onCancel}
70+
onConfirm={this.handleConfirm}
71+
>
72+
{requireTextInput && (
73+
<Field
74+
label={<Label text="Text" />}
75+
input={
76+
<TextInput
77+
multiline
78+
value={this.state.textInput}
79+
onChange={textInput => this.setState({ textInput })}
80+
placeholder="Enter text here..."
81+
/>
82+
}
83+
/>
84+
)}
85+
{requireFileUpload && (
86+
<Field
87+
label={<Label text="File Upload" />}
88+
input={
89+
<FileInput
90+
value={this.state.uploadedFile}
91+
onChange={this.handleFileChange}
92+
/>
93+
}
94+
/>
95+
)}
96+
</Modal>
97+
);
98+
}
99+
}

src/dashboard/Data/Views/CreateViewDialog.react.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export default class CreateViewDialog extends React.Component {
3737
query: '[]',
3838
cloudFunction: '',
3939
showCounter: false,
40+
requireTextInput: false,
41+
requireFileUpload: false,
4042
};
4143
}
4244

@@ -77,6 +79,8 @@ export default class CreateViewDialog extends React.Component {
7779
query: this.state.dataSourceType === DataSourceTypes.query ? JSON.parse(this.state.query) : null,
7880
cloudFunction: this.state.dataSourceType === DataSourceTypes.cloudFunction ? this.state.cloudFunction : null,
7981
showCounter: this.state.showCounter,
82+
requireTextInput: this.state.dataSourceType === DataSourceTypes.cloudFunction ? this.state.requireTextInput : false,
83+
requireFileUpload: this.state.dataSourceType === DataSourceTypes.cloudFunction ? this.state.requireFileUpload : false,
8084
})
8185
}
8286
>
@@ -152,6 +156,28 @@ export default class CreateViewDialog extends React.Component {
152156
/>
153157
}
154158
/>
159+
{this.state.dataSourceType === DataSourceTypes.cloudFunction && (
160+
<>
161+
<Field
162+
label={<Label text="Require text input" description="When checked, users will be prompted to enter text when opening this view." />}
163+
input={
164+
<Checkbox
165+
checked={this.state.requireTextInput}
166+
onChange={requireTextInput => this.setState({ requireTextInput })}
167+
/>
168+
}
169+
/>
170+
<Field
171+
label={<Label text="Require file upload" description="When checked, users will be prompted to upload a file when opening this view." />}
172+
input={
173+
<Checkbox
174+
checked={this.state.requireFileUpload}
175+
onChange={requireFileUpload => this.setState({ requireFileUpload })}
176+
/>
177+
}
178+
/>
179+
</>
180+
)}
155181
</Modal>
156182
);
157183
}

src/dashboard/Data/Views/EditViewDialog.react.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export default class EditViewDialog extends React.Component {
3636
query: view.query ? JSON.stringify(view.query, null, 2) : '[]',
3737
cloudFunction: view.cloudFunction || '',
3838
showCounter: !!view.showCounter,
39+
requireTextInput: !!view.requireTextInput,
40+
requireFileUpload: !!view.requireFileUpload,
3941
};
4042
}
4143

@@ -76,6 +78,8 @@ export default class EditViewDialog extends React.Component {
7678
query: this.state.dataSourceType === 'query' ? JSON.parse(this.state.query) : null,
7779
cloudFunction: this.state.dataSourceType === 'cloudFunction' ? this.state.cloudFunction : null,
7880
showCounter: this.state.showCounter,
81+
requireTextInput: this.state.dataSourceType === 'cloudFunction' ? this.state.requireTextInput : false,
82+
requireFileUpload: this.state.dataSourceType === 'cloudFunction' ? this.state.requireFileUpload : false,
7983
})
8084
}
8185
>
@@ -151,6 +155,28 @@ export default class EditViewDialog extends React.Component {
151155
/>
152156
}
153157
/>
158+
{this.state.dataSourceType === 'cloudFunction' && (
159+
<>
160+
<Field
161+
label={<Label text="Require text input" description="When checked, users will be prompted to enter text when opening this view." />}
162+
input={
163+
<Checkbox
164+
checked={this.state.requireTextInput}
165+
onChange={requireTextInput => this.setState({ requireTextInput })}
166+
/>
167+
}
168+
/>
169+
<Field
170+
label={<Label text="Require file upload" description="When checked, users will be prompted to upload a file when opening this view." />}
171+
input={
172+
<Checkbox
173+
checked={this.state.requireFileUpload}
174+
onChange={requireFileUpload => this.setState({ requireFileUpload })}
175+
/>
176+
}
177+
/>
178+
</>
179+
)}
154180
</Modal>
155181
);
156182
}

src/dashboard/Data/Views/Views.react.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import subscribeTo from 'lib/subscribeTo';
2020
import { withRouter } from 'lib/withRouter';
2121
import Parse from 'parse';
2222
import React from 'react';
23+
import CloudFunctionInputDialog from './CloudFunctionInputDialog.react';
2324
import CreateViewDialog from './CreateViewDialog.react';
2425
import DeleteViewDialog from './DeleteViewDialog.react';
2526
import EditViewDialog from './EditViewDialog.react';
@@ -50,6 +51,8 @@ class Views extends TableView {
5051
lastNote: null,
5152
loading: false,
5253
viewValue: null,
54+
showCloudFunctionInput: false,
55+
cloudFunctionInputConfig: null,
5356
};
5457
this.headersRef = React.createRef();
5558
this.noteTimeout = null;
@@ -142,9 +145,29 @@ class Views extends TableView {
142145
return;
143146
}
144147

148+
// Check if cloud function view requires input
149+
if (view.cloudFunction && (view.requireTextInput || view.requireFileUpload)) {
150+
if (this._isMounted) {
151+
this.setState({
152+
loading: false,
153+
showCloudFunctionInput: true,
154+
cloudFunctionInputConfig: {
155+
view,
156+
requireTextInput: view.requireTextInput,
157+
requireFileUpload: view.requireFileUpload,
158+
},
159+
});
160+
}
161+
return;
162+
}
163+
164+
this.executeCloudFunctionOrQuery(view);
165+
}
166+
167+
executeCloudFunctionOrQuery(view, params = {}) {
145168
// Choose data source: Cloud Function or Aggregation Pipeline
146169
const dataPromise = view.cloudFunction
147-
? Parse.Cloud.run(view.cloudFunction, {}, { useMasterKey: true })
170+
? Parse.Cloud.run(view.cloudFunction, params, { useMasterKey: true })
148171
: new Parse.Query(view.className).aggregate(view.query || [], { useMasterKey: true });
149172

150173
dataPromise
@@ -256,6 +279,13 @@ class Views extends TableView {
256279
}
257280

258281
onRefresh() {
282+
// Clear any existing cloud function input modal first
283+
if (this.state.showCloudFunctionInput) {
284+
this.setState({
285+
showCloudFunctionInput: false,
286+
cloudFunctionInputConfig: null,
287+
});
288+
}
259289
this.loadData(this.props.params.name);
260290
}
261291

@@ -659,6 +689,27 @@ class Views extends TableView {
659689
}}
660690
/>
661691
);
692+
} else if (this.state.showCloudFunctionInput && this.state.cloudFunctionInputConfig) {
693+
const config = this.state.cloudFunctionInputConfig;
694+
extras = (
695+
<CloudFunctionInputDialog
696+
requireTextInput={config.requireTextInput}
697+
requireFileUpload={config.requireFileUpload}
698+
onCancel={() => this.setState({
699+
showCloudFunctionInput: false,
700+
cloudFunctionInputConfig: null,
701+
loading: false,
702+
})}
703+
onConfirm={(params) => {
704+
this.setState({
705+
showCloudFunctionInput: false,
706+
cloudFunctionInputConfig: null,
707+
loading: true,
708+
});
709+
this.executeCloudFunctionOrQuery(config.view, params);
710+
}}
711+
/>
712+
);
662713
}
663714
let notification = null;
664715
if (this.state.lastError) {

0 commit comments

Comments
 (0)