Skip to content

Commit a6f59fd

Browse files
authored
Merge pull request #1309 from processing/feature/public-api-asset-limit
Feature/public api asset limit, Fixes #168
2 parents 5654ed1 + 9de8257 commit a6f59fd

27 files changed

+511
-145
lines changed

client/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export const SHOW_NEW_FOLDER_MODAL = 'SHOW_NEW_FOLDER_MODAL';
8080
export const CLOSE_NEW_FOLDER_MODAL = 'CLOSE_NEW_FOLDER_MODAL';
8181
export const SHOW_FOLDER_CHILDREN = 'SHOW_FOLDER_CHILDREN';
8282
export const HIDE_FOLDER_CHILDREN = 'HIDE_FOLDER_CHILDREN';
83+
export const OPEN_UPLOAD_FILE_MODAL = 'OPEN_UPLOAD_FILE_MODAL';
84+
export const CLOSE_UPLOAD_FILE_MODAL = 'CLOSE_UPLOAD_FILE_MODAL';
8385

8486
export const SHOW_SHARE_MODAL = 'SHOW_SHARE_MODAL';
8587
export const CLOSE_SHARE_MODAL = 'CLOSE_SHARE_MODAL';
@@ -127,6 +129,7 @@ export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE';
127129
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
128130
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
129131
export const SET_ASSETS = 'SET_ASSETS';
132+
export const DELETE_ASSET = 'DELETE_ASSET';
130133

131134
export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
132135
export const SET_SORTING = 'SET_SORTING';

client/modules/IDE/actions/assets.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,23 @@ export function getAssets() {
3030
};
3131
}
3232

33-
export function deleteAsset(assetKey, userId) {
33+
export function deleteAsset(assetKey) {
3434
return {
35-
type: 'PLACEHOLDER'
35+
type: ActionTypes.DELETE_ASSET,
36+
key: assetKey
37+
};
38+
}
39+
40+
export function deleteAssetRequest(assetKey) {
41+
return (dispatch) => {
42+
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true })
43+
.then((response) => {
44+
dispatch(deleteAsset(assetKey));
45+
})
46+
.catch(() => {
47+
dispatch({
48+
type: ActionTypes.ERROR
49+
});
50+
});
3651
};
3752
}

client/modules/IDE/actions/ide.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,19 @@ export function closeNewFileModal() {
7575
};
7676
}
7777

78+
export function openUploadFileModal(parentId) {
79+
return {
80+
type: ActionTypes.OPEN_UPLOAD_FILE_MODAL,
81+
parentId
82+
};
83+
}
84+
85+
export function closeUploadFileModal() {
86+
return {
87+
type: ActionTypes.CLOSE_UPLOAD_FILE_MODAL
88+
};
89+
}
90+
7891
export function expandSidebar() {
7992
return {
8093
type: ActionTypes.EXPAND_SIDEBAR

client/modules/IDE/actions/uploader.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,11 @@ export function dropzoneAcceptCallback(userId, file, done) {
6666
done();
6767
})
6868
.catch((response) => {
69-
file.custom_status = 'rejected'; // eslint-disable-line
70-
if (response.data.responseText && response.data.responseText.message) {
69+
file.custom_status = 'rejected'; // eslint-disable-line
70+
if (response.data && response.data.responseText && response.data.responseText.message) {
7171
done(response.data.responseText.message);
7272
}
73-
done('error preparing the upload');
73+
done('Error: Reached upload limit.');
7474
});
7575
}
7676
};

client/modules/IDE/components/AssetList.jsx

Lines changed: 143 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,146 @@ import { bindActionCreators } from 'redux';
55
import { Link } from 'react-router';
66
import { Helmet } from 'react-helmet';
77
import prettyBytes from 'pretty-bytes';
8+
import InlineSVG from 'react-inlinesvg';
89

910
import Loader from '../../App/components/loader';
1011
import * as AssetActions from '../actions/assets';
12+
import downFilledTriangle from '../../../images/down-filled-triangle.svg';
13+
14+
class AssetListRowBase extends React.Component {
15+
constructor(props) {
16+
super(props);
17+
this.state = {
18+
isFocused: false,
19+
optionsOpen: false
20+
};
21+
}
22+
23+
onFocusComponent = () => {
24+
this.setState({ isFocused: true });
25+
}
26+
27+
onBlurComponent = () => {
28+
this.setState({ isFocused: false });
29+
setTimeout(() => {
30+
if (!this.state.isFocused) {
31+
this.closeOptions();
32+
}
33+
}, 200);
34+
}
35+
36+
openOptions = () => {
37+
this.setState({
38+
optionsOpen: true
39+
});
40+
}
41+
42+
closeOptions = () => {
43+
this.setState({
44+
optionsOpen: false
45+
});
46+
}
47+
48+
toggleOptions = () => {
49+
if (this.state.optionsOpen) {
50+
this.closeOptions();
51+
} else {
52+
this.openOptions();
53+
}
54+
}
55+
56+
handleDropdownOpen = () => {
57+
this.closeOptions();
58+
this.openOptions();
59+
}
60+
61+
handleAssetDelete = () => {
62+
const { key, name } = this.props.asset;
63+
this.closeOptions();
64+
if (window.confirm(`Are you sure you want to delete "${name}"?`)) {
65+
this.props.deleteAssetRequest(key);
66+
}
67+
}
68+
69+
render() {
70+
const { asset, username } = this.props;
71+
const { optionsOpen } = this.state;
72+
return (
73+
<tr className="asset-table__row" key={asset.key}>
74+
<th scope="row">
75+
<Link to={asset.url} target="_blank">
76+
{asset.name}
77+
</Link>
78+
</th>
79+
<td>{prettyBytes(asset.size)}</td>
80+
<td>
81+
{ asset.sketchId && <Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link> }
82+
</td>
83+
<td className="asset-table__dropdown-column">
84+
<button
85+
className="asset-table__dropdown-button"
86+
onClick={this.toggleOptions}
87+
onBlur={this.onBlurComponent}
88+
onFocus={this.onFocusComponent}
89+
>
90+
<InlineSVG src={downFilledTriangle} alt="Menu" />
91+
</button>
92+
{optionsOpen &&
93+
<ul
94+
className="asset-table__action-dialogue"
95+
>
96+
<li>
97+
<button
98+
className="asset-table__action-option"
99+
onClick={this.handleAssetDelete}
100+
onBlur={this.onBlurComponent}
101+
onFocus={this.onFocusComponent}
102+
>
103+
Delete
104+
</button>
105+
</li>
106+
<li>
107+
<Link
108+
to={asset.url}
109+
target="_blank"
110+
onBlur={this.onBlurComponent}
111+
onFocus={this.onFocusComponent}
112+
className="asset-table__action-option"
113+
>
114+
Open in New Tab
115+
</Link>
116+
</li>
117+
</ul>}
118+
</td>
119+
</tr>
120+
);
121+
}
122+
}
123+
124+
AssetListRowBase.propTypes = {
125+
asset: PropTypes.shape({
126+
key: PropTypes.string.isRequired,
127+
url: PropTypes.string.isRequired,
128+
sketchId: PropTypes.string,
129+
sketchName: PropTypes.string,
130+
name: PropTypes.string.isRequired,
131+
size: PropTypes.number.isRequired
132+
}).isRequired,
133+
deleteAssetRequest: PropTypes.func.isRequired,
134+
username: PropTypes.string.isRequired
135+
};
136+
137+
function mapStateToPropsAssetListRow(state) {
138+
return {
139+
username: state.user.username
140+
};
141+
}
142+
143+
function mapDispatchToPropsAssetListRow(dispatch) {
144+
return bindActionCreators(AssetActions, dispatch);
145+
}
146+
147+
const AssetListRow = connect(mapStateToPropsAssetListRow, mapDispatchToPropsAssetListRow)(AssetListRowBase);
11148

12149
class AssetList extends React.Component {
13150
constructor(props) {
@@ -16,10 +153,7 @@ class AssetList extends React.Component {
16153
}
17154

18155
getAssetsTitle() {
19-
if (!this.props.username || this.props.username === this.props.user.username) {
20-
return 'p5.js Web Editor | My assets';
21-
}
22-
return `p5.js Web Editor | ${this.props.username}'s assets`;
156+
return 'p5.js Web Editor | My assets';
23157
}
24158

25159
hasAssets() {
@@ -39,7 +173,6 @@ class AssetList extends React.Component {
39173
}
40174

41175
render() {
42-
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
43176
const { assetList } = this.props;
44177
return (
45178
<div className="asset-table-container">
@@ -52,24 +185,14 @@ class AssetList extends React.Component {
52185
<table className="asset-table">
53186
<thead>
54187
<tr>
55-
<th scope="col">Name</th>
56-
<th scope="col">Size</th>
57-
<th scope="col">Sketch</th>
188+
<th>Name</th>
189+
<th>Size</th>
190+
<th>Sketch</th>
191+
<th scope="col"></th>
58192
</tr>
59193
</thead>
60194
<tbody>
61-
{assetList.map(asset =>
62-
(
63-
<tr className="asset-table__row" key={asset.key}>
64-
<th scope="row">
65-
<Link to={asset.url} target="_blank">
66-
{asset.name}
67-
</Link>
68-
</th>
69-
<td>{prettyBytes(asset.size)}</td>
70-
<td><Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
71-
</tr>
72-
))}
195+
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
73196
</tbody>
74197
</table>}
75198
</div>
@@ -81,7 +204,6 @@ AssetList.propTypes = {
81204
user: PropTypes.shape({
82205
username: PropTypes.string
83206
}).isRequired,
84-
username: PropTypes.string.isRequired,
85207
assetList: PropTypes.arrayOf(PropTypes.shape({
86208
key: PropTypes.string.isRequired,
87209
name: PropTypes.string.isRequired,
@@ -97,7 +219,6 @@ function mapStateToProps(state) {
97219
return {
98220
user: state.user,
99221
assetList: state.assets.list,
100-
totalSize: state.assets.totalSize,
101222
loading: state.loading
102223
};
103224
}

client/modules/IDE/components/AssetSize.jsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import React from 'react';
33
import { connect } from 'react-redux';
44
import prettyBytes from 'pretty-bytes';
55

6-
const MB_TO_B = 1000 * 1000;
7-
const MAX_SIZE_B = 250 * MB_TO_B;
6+
const __process = (typeof global !== 'undefined' ? global : window).process;
7+
const limit = __process.env.UPLOAD_LIMIT || 250000000;
8+
const MAX_SIZE_B = limit;
89

910
const formatPercent = (percent) => {
1011
const percentUsed = percent * 100;
@@ -17,17 +18,18 @@ const formatPercent = (percent) => {
1718

1819
/* Eventually, this copy should be Total / 250 MB Used */
1920
const AssetSize = ({ totalSize }) => {
20-
if (!totalSize) {
21+
if (totalSize === undefined) {
2122
return null;
2223
}
2324

2425
const currentSize = prettyBytes(totalSize);
2526
const sizeLimit = prettyBytes(MAX_SIZE_B);
2627
const percentValue = totalSize / MAX_SIZE_B;
2728
const percent = formatPercent(percentValue);
29+
const percentSize = percentValue < 1 ? percentValue : 1;
2830

2931
return (
30-
<div className="asset-size" style={{ '--percent': percentValue }}>
32+
<div className="asset-size" style={{ '--percent': percentSize }}>
3133
<div className="asset-size-bar" />
3234
<p className="asset-current">{currentSize} ({percent})</p>
3335
<p className="asset-max">Max: {sizeLimit}</p>
@@ -42,7 +44,7 @@ AssetSize.propTypes = {
4244
function mapStateToProps(state) {
4345
return {
4446
user: state.user,
45-
totalSize: state.assets.totalSize,
47+
totalSize: state.user.totalSize || state.assets.totalSize,
4648
};
4749
}
4850

client/modules/IDE/components/FileUploader.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class FileUploader extends React.Component {
3030
thumbnailWidth: 200,
3131
thumbnailHeight: 200,
3232
acceptedFiles: fileExtensionsAndMimeTypes,
33-
dictDefaultMessage: 'Drop files here to upload or click to use the file browser',
33+
dictDefaultMessage: 'Drop files here or click to use the file browser',
3434
accept: this.props.dropzoneAcceptCallback.bind(this, userId),
3535
sending: this.props.dropzoneSendingCallback,
3636
complete: this.props.dropzoneCompleteCallback

client/modules/IDE/components/NewFileForm.jsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@ class NewFileForm extends React.Component {
2222
handleSubmit(this.createFile)(data);
2323
}}
2424
>
25-
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
26-
<input
27-
className="new-file-form__name-input"
28-
id="name"
29-
type="text"
30-
placeholder="Name"
31-
{...domOnlyProps(name)}
32-
ref={(element) => { this.fileName = element; }}
33-
/>
34-
<input type="submit" value="Add File" aria-label="add file" />
25+
<div className="new-file-form__input-wrapper">
26+
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
27+
<input
28+
className="new-file-form__name-input"
29+
id="name"
30+
type="text"
31+
placeholder="Name"
32+
{...domOnlyProps(name)}
33+
ref={(element) => { this.fileName = element; }}
34+
/>
35+
<input type="submit" value="Add File" aria-label="add file" />
36+
</div>
3537
{name.touched && name.error && <span className="form-error">{name.error}</span>}
3638
</form>
3739
);

0 commit comments

Comments
 (0)