Skip to content

Commit 35f4dbe

Browse files
committed
Merge branch 'develop' into fix/hot-reload
2 parents cf6773e + 01dca7f commit 35f4dbe

30 files changed

+27567
-60585
lines changed

.github/workflows/deploy.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ jobs:
2323
with:
2424
ref: release
2525
- name: Set up Docker Buildx
26-
uses: docker/setup-buildx-action@v1
26+
uses: docker/setup-buildx-action@v2
27+
with:
28+
platforms: linux/amd64,linux/arm64
2729
- name: Login to Docker Hub
2830
uses: docker/login-action@v1
2931
with:

client/components/Nav.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class Nav extends React.PureComponent {
3030
super(props);
3131
this.handleSave = this.handleSave.bind(this);
3232
this.handleNew = this.handleNew.bind(this);
33+
this.handleDuplicate = this.handleDuplicate.bind(this);
3334
this.handleShare = this.handleShare.bind(this);
3435
this.handleDownload = this.handleDownload.bind(this);
3536
this.handleLangSelection = this.handleLangSelection.bind(this);
@@ -56,6 +57,10 @@ class Nav extends React.PureComponent {
5657
}
5758
}
5859

60+
handleDuplicate() {
61+
this.props.cloneProject();
62+
}
63+
5964
handleLangSelection(event) {
6065
this.props.setLanguage(event.target.value);
6166
this.props.showToast(1500);
@@ -132,7 +137,7 @@ class Nav extends React.PureComponent {
132137
</NavMenuItem>
133138
<NavMenuItem
134139
hideIf={!this.props.project.id || !this.props.user.authenticated}
135-
onClick={this.props.cloneProject}
140+
onClick={this.handleDuplicate}
136141
>
137142
{this.props.t('Nav.File.Duplicate')}
138143
</NavMenuItem>

client/modules/IDE/actions/uploader.js

Lines changed: 46 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,68 @@
1+
import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils';
12
import apiClient from '../../../utils/apiClient';
23
import getConfig from '../../../utils/getConfig';
34
import { handleCreateFile } from './files';
4-
import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils';
55

6-
const s3BucketHttps =
6+
export const s3BucketHttps =
77
getConfig('S3_BUCKET_URL_BASE') ||
88
`https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig(
99
'S3_BUCKET'
1010
)}/`;
1111
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
1212

13-
function localIntercept(file, options = {}) {
14-
return new Promise((resolve, reject) => {
15-
if (!options.readType) {
16-
// const mime = file.type;
17-
// const textType = a(_textTypes).any(type => {
18-
// const re = new RegExp(type);
19-
// return re.test(mime);
20-
// });
21-
// options.readType = textType ? 'readAsText' : 'readAsDataURL';
22-
options.readType = 'readAsText'; // eslint-disable-line
23-
}
24-
const reader = new window.FileReader();
25-
reader.onload = () => {
26-
resolve(reader.result);
27-
};
28-
reader.onerror = () => {
29-
reject(reader.result);
30-
};
31-
32-
// run the reader
33-
reader[options.readType](file);
34-
});
35-
}
36-
37-
function toBinary(string) {
38-
const codeUnits = new Uint16Array(string.length);
39-
for (let i = 0; i < codeUnits.length; i += 1) {
40-
codeUnits[i] = string.charCodeAt(i);
41-
}
42-
return String.fromCharCode(...new Uint8Array(codeUnits.buffer));
13+
function isS3Upload(file) {
14+
return !TEXT_FILE_REGEX.test(file.name) || file.size >= MAX_LOCAL_FILE_SIZE;
4315
}
4416

45-
export function dropzoneAcceptCallback(userId, file, done) {
46-
return () => {
47-
// if a user would want to edit this file as text, local interceptor
48-
if (file.name.match(TEXT_FILE_REGEX) && file.size < MAX_LOCAL_FILE_SIZE) {
49-
localIntercept(file)
50-
.then((result) => {
51-
file.content = result; // eslint-disable-line
52-
done('Uploading plaintext file locally.');
53-
file.previewElement.classList.remove('dz-error');
54-
file.previewElement.classList.add('dz-success');
55-
file.previewElement.classList.add('dz-processing');
56-
file.previewElement.querySelector('.dz-upload').style.width = '100%';
57-
})
58-
.catch((result) => {
59-
done(`Failed to download file ${file.name}: ${result}`);
60-
console.warn(file);
61-
});
62-
} else {
63-
file.postData = []; // eslint-disable-line
64-
apiClient
65-
.post('/S3/sign', {
66-
name: file.name,
67-
type: file.type,
68-
size: file.size,
69-
userId
70-
// _csrf: document.getElementById('__createPostToken').value
71-
})
72-
.then((response) => {
73-
file.custom_status = 'ready'; // eslint-disable-line
74-
file.postData = response.data; // eslint-disable-line
75-
file.s3 = response.data.key; // eslint-disable-line
76-
file.previewTemplate.className += ' uploading'; // eslint-disable-line
77-
done();
78-
})
79-
.catch((error) => {
80-
const { response } = error;
81-
file.custom_status = 'rejected'; // eslint-disable-line
82-
if (
83-
response.data &&
84-
response.data.responseText &&
85-
response.data.responseText.message
86-
) {
87-
done(response.data.responseText.message);
88-
}
89-
done('Error: Reached upload limit.');
90-
});
17+
export async function dropzoneAcceptCallback(userId, file, done) {
18+
// if a user would want to edit this file as text, local interceptor
19+
if (!isS3Upload(file)) {
20+
try {
21+
// eslint-disable-next-line no-param-reassign
22+
file.content = await file.text();
23+
// Make it an error so that it won't be sent to S3, but style as a success.
24+
done('Uploading plaintext file locally.');
25+
file.previewElement.classList.remove('dz-error');
26+
file.previewElement.classList.add('dz-success');
27+
file.previewElement.classList.add('dz-processing');
28+
file.previewElement.querySelector('.dz-upload').style.width = '100%';
29+
} catch (error) {
30+
done(`Failed to download file ${file.name}: ${error}`);
31+
console.warn(file);
9132
}
92-
};
33+
} else {
34+
try {
35+
const response = await apiClient.post('/S3/sign', {
36+
name: file.name,
37+
type: file.type,
38+
size: file.size,
39+
userId
40+
// _csrf: document.getElementById('__createPostToken').value
41+
});
42+
// eslint-disable-next-line no-param-reassign
43+
file.postData = response.data;
44+
done();
45+
} catch (error) {
46+
done(
47+
error?.response?.data?.responseText?.message ||
48+
error?.message ||
49+
'Error: Reached upload limit.'
50+
);
51+
}
52+
}
9353
}
9454

9555
export function dropzoneSendingCallback(file, xhr, formData) {
96-
return () => {
97-
if (!file.name.match(TEXT_FILE_REGEX) || file.size >= MAX_LOCAL_FILE_SIZE) {
98-
Object.keys(file.postData).forEach((key) => {
99-
formData.append(key, file.postData[key]);
100-
});
101-
}
102-
};
56+
if (isS3Upload(file)) {
57+
Object.keys(file.postData).forEach((key) => {
58+
formData.append(key, file.postData[key]);
59+
});
60+
}
10361
}
10462

10563
export function dropzoneCompleteCallback(file) {
106-
return (dispatch) => { // eslint-disable-line
107-
if (
108-
(!file.name.match(TEXT_FILE_REGEX) || file.size >= MAX_LOCAL_FILE_SIZE) &&
109-
file.status !== 'error'
110-
) {
111-
let inputHidden = '<input type="hidden" name="attachments[]" value="';
112-
const json = {
113-
url: `${s3BucketHttps}${file.postData.key}`,
114-
originalFilename: file.name
115-
};
116-
117-
let jsonStr = JSON.stringify(json);
118-
119-
// convert the json string to binary data so that btoa can encode it
120-
jsonStr = toBinary(jsonStr);
121-
inputHidden += `${window.btoa(jsonStr)}" />`;
122-
document.getElementById('uploader').innerHTML += inputHidden;
123-
64+
return (dispatch) => {
65+
if (isS3Upload(file) && file.postData && file.status !== 'error') {
12466
const formParams = {
12567
name: file.name,
12668
url: `${s3BucketHttps}${file.postData.key}`

client/modules/IDE/components/Editor.jsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ class Editor extends React.Component {
159159
[`${metaKey}-Enter`]: () => null,
160160
[`Shift-${metaKey}-Enter`]: () => null,
161161
[`${metaKey}-F`]: 'findPersistent',
162+
[`Shift-${metaKey}-F`]: this.tidyCode,
162163
[`${metaKey}-G`]: 'findPersistentNext',
163164
[`Shift-${metaKey}-G`]: 'findPersistentPrev',
164165
[replaceCommand]: 'replace',
@@ -194,16 +195,6 @@ class Editor extends React.Component {
194195
});
195196

196197
this._cm.on('keydown', (_cm, e) => {
197-
if (
198-
((metaKey === 'Cmd' && e.metaKey) ||
199-
(metaKey === 'Ctrl' && e.ctrlKey)) &&
200-
e.shiftKey &&
201-
e.key === 'f'
202-
) {
203-
e.preventDefault();
204-
this.tidyCode();
205-
}
206-
207198
// Show hint
208199
const mode = this._cm.getOption('mode');
209200
if (/^[a-z]$/i.test(e.key) && (mode === 'css' || mode === 'javascript')) {
Lines changed: 49 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,69 @@
1-
import PropTypes from 'prop-types';
2-
import React from 'react';
31
import Dropzone from 'dropzone';
4-
import { bindActionCreators } from 'redux';
5-
import { connect } from 'react-redux';
6-
import { withTranslation } from 'react-i18next';
7-
import * as UploaderActions from '../actions/uploader';
8-
import getConfig from '../../../utils/getConfig';
2+
import React, { useEffect } from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
import { useDispatch, useSelector } from 'react-redux';
5+
import styled from 'styled-components';
96
import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils';
7+
import { remSize } from '../../../theme';
8+
import {
9+
dropzoneAcceptCallback,
10+
dropzoneCompleteCallback,
11+
dropzoneSendingCallback,
12+
s3BucketHttps
13+
} from '../actions/uploader';
1014

11-
const s3Bucket =
12-
getConfig('S3_BUCKET_URL_BASE') ||
13-
`https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig(
14-
'S3_BUCKET'
15-
)}/`;
15+
Dropzone.autoDiscover = false;
1616

17-
class FileUploader extends React.Component {
18-
componentDidMount() {
19-
this.createDropzone();
20-
Dropzone.autoDiscover = false;
17+
// TODO: theming for dark vs. light theme
18+
// TODO: include color and background-color settings after migrating the themify variables.
19+
const StyledUploader = styled.div`
20+
min-height: ${remSize(200)};
21+
width: 100%;
22+
text-align: center;
23+
.dz-preview.dz-image-preview {
24+
background-color: transparent;
2125
}
26+
`;
2227

23-
createDropzone() {
24-
const userId = this.props.project.owner
25-
? this.props.project.owner.id
26-
: this.props.user.id;
27-
this.uploader = new Dropzone('div#uploader', {
28-
url: s3Bucket,
28+
function FileUploader() {
29+
const { t } = useTranslation();
30+
const dispatch = useDispatch();
31+
const userId = useSelector((state) => state.user.id);
32+
33+
useEffect(() => {
34+
const uploader = new Dropzone('div#uploader', {
35+
url: s3BucketHttps,
2936
method: 'post',
3037
autoProcessQueue: true,
3138
clickable: true,
39+
hiddenInputContainer: '#hidden-input-container',
3240
maxFiles: 6,
3341
parallelUploads: 2,
3442
maxFilesize: 5, // in mb
3543
maxThumbnailFilesize: 8, // 8 mb
3644
thumbnailWidth: 200,
3745
thumbnailHeight: 200,
3846
acceptedFiles: fileExtensionsAndMimeTypes,
39-
dictDefaultMessage: this.props.t('FileUploader.DictDefaultMessage'),
40-
accept: this.props.dropzoneAcceptCallback.bind(this, userId),
41-
sending: this.props.dropzoneSendingCallback,
42-
complete: this.props.dropzoneCompleteCallback
43-
// error: (file, errorMessage) => {
44-
// console.log(file);
45-
// console.log(errorMessage);
46-
// }
47+
dictDefaultMessage: t('FileUploader.DictDefaultMessage'),
48+
accept: (file, done) => {
49+
dropzoneAcceptCallback(userId, file, done);
50+
},
51+
sending: dropzoneSendingCallback
4752
});
48-
}
49-
50-
render() {
51-
return <div id="uploader" className="uploader dropzone"></div>;
52-
}
53-
}
54-
55-
FileUploader.propTypes = {
56-
dropzoneAcceptCallback: PropTypes.func.isRequired,
57-
dropzoneSendingCallback: PropTypes.func.isRequired,
58-
dropzoneCompleteCallback: PropTypes.func.isRequired,
59-
project: PropTypes.shape({
60-
owner: PropTypes.shape({
61-
id: PropTypes.string
62-
})
63-
}),
64-
user: PropTypes.shape({
65-
id: PropTypes.string
66-
}),
67-
t: PropTypes.func.isRequired
68-
};
69-
70-
FileUploader.defaultProps = {
71-
project: {
72-
id: undefined,
73-
owner: undefined
74-
},
75-
user: {
76-
id: undefined
77-
}
78-
};
79-
80-
function mapStateToProps(state) {
81-
return {
82-
files: state.files,
83-
project: state.project,
84-
user: state.user
85-
};
86-
}
53+
uploader.on('complete', (file) => {
54+
dispatch(dropzoneCompleteCallback(file));
55+
});
56+
return () => {
57+
uploader.destroy();
58+
};
59+
}, [userId, t, dispatch]);
8760

88-
function mapDispatchToProps(dispatch) {
89-
return bindActionCreators(UploaderActions, dispatch);
61+
return (
62+
<div>
63+
<StyledUploader id="uploader" className="dropzone" />
64+
<div id="hidden-input-container" />
65+
</div>
66+
);
9067
}
9168

92-
export default withTranslation()(
93-
connect(mapStateToProps, mapDispatchToProps)(FileUploader)
94-
);
69+
export default FileUploader;

client/modules/IDE/components/UploadFileModal.jsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ const UploadFileModal = () => {
3434
.
3535
</p>
3636
) : (
37-
<div>
38-
<FileUploader />
39-
</div>
37+
<FileUploader />
4038
)}
4139
</Modal>
4240
);

0 commit comments

Comments
 (0)