Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
Add plugin FilesWillUpload hook (#1777)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwilander authored and saturninoabril committed Sep 27, 2018
1 parent 52774e8 commit 13d6571
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 46 deletions.
79 changes: 38 additions & 41 deletions components/file_upload/file_upload.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export default class FileUpload extends PureComponent {
* Plugin file upload methods to be added
*/
pluginFileUploadMethods: PropTypes.arrayOf(PropTypes.object),
pluginFilesWillUploadHooks: PropTypes.arrayOf(PropTypes.object),
};

static contextTypes = {
Expand All @@ -132,6 +133,7 @@ export default class FileUpload extends PureComponent {

static defaultProps = {
pluginFileUploadMethods: [],
pluginFilesWillUploadHooks: [],
};

constructor(props) {
Expand Down Expand Up @@ -184,12 +186,36 @@ export default class FileUpload extends PureComponent {
this.props.onUploadError(err, clientId, this.props.currentChannelId);
}

uploadFiles = (files) => {
const sortedFiles = sortFilesByName(files);
pluginUploadFiles = (files) => {
// clear any existing errors
this.props.onUploadError(null);
this.uploadFiles(files);
}

checkPluginHooksAndUploadFiles = (files) => {
// clear any existing errors
this.props.onUploadError(null);

let sortedFiles = sortFilesByName(files);

const willUploadHooks = this.props.pluginFilesWillUploadHooks;
for (const h of willUploadHooks) {
const result = h.hook(sortedFiles, this.pluginUploadFiles);

// Display an error message if there is one but don't reject the upload
if (result.message) {
this.props.onUploadError(result.message);
}

sortedFiles = result.files;
}

if (sortedFiles) {
this.uploadFiles(sortedFiles);
}
}

uploadFiles = (sortedFiles) => {
const {currentChannelId} = this.props;

const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.fileCount;
Expand Down Expand Up @@ -221,9 +247,7 @@ export default class FileUpload extends PureComponent {
(e) => this.fileUploadFail(e, clientId)
);

const requests = this.state.requests;
requests[clientId] = request;
this.setState({requests});
this.setState({requests: {...this.state.requests, [clientId]: request}});
clientIds.push(clientId);

numUploads += 1;
Expand Down Expand Up @@ -260,7 +284,7 @@ export default class FileUpload extends PureComponent {

handleChange = (e) => {
if (e.target.files.length > 0) {
this.uploadFiles(e.target.files);
this.checkPluginHooksAndUploadFiles(e.target.files);

clearFileInput(e.target);
}
Expand All @@ -279,7 +303,7 @@ export default class FileUpload extends PureComponent {
var files = e.originalEvent.dataTransfer.files;

if (typeof files !== 'string' && files.length) {
this.uploadFiles(files);
this.checkPluginHooksAndUploadFiles(files);
}

this.props.onFileUploadChange();
Expand Down Expand Up @@ -364,31 +388,20 @@ export default class FileUpload extends PureComponent {
items.push(item);
}

// This looks redundant, but must be done this way due to
// setState being an asynchronous call
if (items && items.length > 0) {
if (!this.props.canUploadFiles) {
this.props.onUploadError(localizeMessage('file_upload.disabled', 'File attachments are disabled.'));
return;
}
const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.fileCount;
var numToUpload = Math.min(uploadsRemaining, items.length);

if (items.length > numToUpload) {
this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES}));
}

const {currentChannelId} = this.props;
const files = [];

for (var i = 0; i < items.length && i < numToUpload; i++) {
var file = items[i].getAsFile();
for (let i = 0; i < items.length; i++) {
const file = items[i].getAsFile();
if (!file) {
continue;
}

// generate a unique id that can be used by other components to refer back to this file upload
const clientId = generateId();

var d = new Date();
let hour = d.getHours();
hour = hour < 10 ? `0${hour}` : `${hour}`;
Expand All @@ -407,23 +420,11 @@ export default class FileUpload extends PureComponent {

const name = formatMessage(holders.pasted) + d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + hour + '-' + minute + ext;

const request = this.props.uploadFile(
file,
name,
currentChannelId,
clientId,
(data) => this.fileUploadSuccess(data),
(err) => this.fileUploadFail(err, clientId)
);

const requests = this.state.requests;
requests[clientId] = request;
this.setState({requests});

this.props.onUploadStart([clientId], currentChannelId);
files.push(new File([file], name));
}

if (numToUpload > 0) {
if (files.length > 0) {
this.checkPluginHooksAndUploadFiles(files);
this.props.onFileUploadChange();
}
}
Expand Down Expand Up @@ -484,10 +485,6 @@ export default class FileUpload extends PureComponent {
this.setState({menuOpen: false});
}

pluginUploadFiles = (files) => {
this.uploadFiles(files);
}

render() {
let multiple = true;
if (isMobileApp()) {
Expand Down Expand Up @@ -528,7 +525,7 @@ export default class FileUpload extends PureComponent {
key={item.pluginId + '_fileuploadpluginmenuitem'}
onClick={() => {
if (item.action) {
item.action(this.pluginUploadFiles);
item.action(this.checkPluginHooksAndUploadFiles);
}
this.setState({menuOpen: false});
}}
Expand Down
1 change: 1 addition & 0 deletions components/file_upload/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function mapStateToProps(state) {
maxFileSize,
canUploadFiles: canUploadFiles(config),
pluginFileUploadMethods: state.plugins.components.FileUploadMethod,
pluginFilesWillUploadHooks: state.plugins.components.FilesWillUploadHook,
};
}

Expand Down
25 changes: 24 additions & 1 deletion plugins/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,30 @@ export default class PluginRegistry {
return id;
}

// Unregister a component using the unique identifier returned after registration.
// Register a hook to intercept file uploads before they take place.
// Accepts a function to run before files get uploaded. Receives an array of
// files and a function to upload files at a later time as arguments. Must
// return an object that can contain two properties:
// - message - An error message to display, leave blank or null to display no message
// - files - Modified array of files to upload, set to null to reject all files
// Returns a unique identifier.
registerFilesWillUploadHook(hook) {
const id = generateId();

store.dispatch({
type: ActionTypes.RECEIVED_PLUGIN_COMPONENT,
name: 'FilesWillUploadHook',
data: {
id,
pluginId: this.id,
hook,
},
});

return id;
}

// Unregister a component, action or hook using the unique identifier returned after registration.
// Accepts a string id.
// Returns undefined in all cases.
unregisterComponent(componentId) {
Expand Down
57 changes: 53 additions & 4 deletions tests/components/file_upload/file_upload.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ describe('components/FileUpload', () => {
<FileUpload {...props}/>
);

wrapper.instance().uploadFiles(files);
wrapper.instance().checkPluginHooksAndUploadFiles(files);

expect(uploadFile).toHaveBeenCalledTimes(2);

Expand All @@ -170,7 +170,7 @@ describe('components/FileUpload', () => {
<FileUpload {...props}/>
);

wrapper.instance().uploadFiles(files);
wrapper.instance().checkPluginHooksAndUploadFiles(files);

expect(uploadFile).not.toBeCalled();

Expand All @@ -192,7 +192,7 @@ describe('components/FileUpload', () => {
<FileUpload {...props}/>
);

wrapper.instance().uploadFiles(files);
wrapper.instance().checkPluginHooksAndUploadFiles(files);

expect(uploadFile).not.toBeCalled();

Expand All @@ -213,7 +213,7 @@ describe('components/FileUpload', () => {
<FileUpload {...props}/>
);

wrapper.instance().uploadFiles(files);
wrapper.instance().checkPluginHooksAndUploadFiles(files);

expect(uploadFile).not.toBeCalled();

Expand Down Expand Up @@ -269,4 +269,53 @@ describe('components/FileUpload', () => {
expect(onFileUploadChange).toBeCalled();
expect(onFileUploadChange).toHaveBeenCalledWith();
});

test('FilesWillUploadHook - should reject all files', () => {
const onUploadError = jest.fn();
const uploadFile = jest.fn();
const onUploadStart = jest.fn();
const pluginHook = () => {
return {files: null};
};
const props = {...baseProps, onUploadError, uploadFile, onUploadStart, pluginFilesWillUploadHooks: [{hook: pluginHook}]};
const files = [{name: 'file1.pdf'}, {name: 'file2.jpg'}];

const wrapper = shallowWithIntl(
<FileUpload {...props}/>
);

wrapper.instance().checkPluginHooksAndUploadFiles(files);

expect(uploadFile).toHaveBeenCalledTimes(0);

expect(onUploadStart).toHaveBeenCalledTimes(0);

expect(onUploadError).toHaveBeenCalledTimes(1);
expect(onUploadError).toHaveBeenCalledWith(null);
});

test('FilesWillUploadHook - should reject one file and allow one file', () => {
const onUploadError = jest.fn();
const uploadFile = jest.fn();
const onUploadStart = jest.fn();
const pluginHook = (files) => {
return {files: files.filter((f) => f.name === 'file1.pdf')};
};
const props = {...baseProps, onUploadError, uploadFile, onUploadStart, pluginFilesWillUploadHooks: [{hook: pluginHook}]};
const files = [{name: 'file1.pdf'}, {name: 'file2.jpg'}];

const wrapper = shallowWithIntl(
<FileUpload {...props}/>
);

wrapper.instance().checkPluginHooksAndUploadFiles(files);

expect(uploadFile).toHaveBeenCalledTimes(1);

expect(onUploadStart).toHaveBeenCalledTimes(1);
expect(onUploadStart).toHaveBeenCalledWith(['generated_id_1'], props.currentChannelId);

expect(onUploadError).toHaveBeenCalledTimes(1);
expect(onUploadError).toHaveBeenCalledWith(null);
});
});

0 comments on commit 13d6571

Please sign in to comment.