Commit f631eea6 authored by Jesse Hallam's avatar Jesse Hallam Committed by Joram Wilander
Browse files

[PLT-8173] Add username and profile picture to webhook set up pages (#514)

* [PLT-8173] Add username and profile picture to webhook set up pages

* [PLT-8173] Strip the post_ prefix on incoming webhook overrides.

* [PLT-8173] fix style issues with newly checked *.js files

* fix redux mapping when editing incoming webhook

This fixes an issue where server-side error messages sent down after
editing an incoming webhook don't show up on the component. This wasn't
previously visible, as until usernames and icon urls could be
overridden, no server side errors would generally occur as such.
parent cd62ffbb
......@@ -39,6 +39,16 @@ export default class AbstractIncomingWebhook extends React.Component {
*/
initialHook: PropTypes.object,
/**
* Whether to allow configuration of the default post username.
*/
enablePostUsernameOverride: PropTypes.bool.isRequired,
/**
* Whether to allow configuration of the default post icon.
*/
enablePostIconOverride: PropTypes.bool.isRequired,
/**
* The async function to run when the action button is pressed
*/
......@@ -56,6 +66,8 @@ export default class AbstractIncomingWebhook extends React.Component {
displayName: hook.display_name || '',
description: hook.description || '',
channelId: hook.channel_id || '',
username: hook.username || '',
iconURL: hook.icon_url || '',
saving: false,
serverError: '',
clientError: null
......@@ -92,7 +104,9 @@ export default class AbstractIncomingWebhook extends React.Component {
const hook = {
channel_id: this.state.channelId,
display_name: this.state.displayName,
description: this.state.description
description: this.state.description,
username: this.state.username,
icon_url: this.state.iconURL
};
this.props.action(hook).then(() => this.setState({saving: false}));
......@@ -116,6 +130,18 @@ export default class AbstractIncomingWebhook extends React.Component {
});
}
updateUsername = (e) => {
this.setState({
username: e.target.value
});
}
updateIconURL = (e) => {
this.setState({
iconURL: e.target.value
});
}
render() {
var headerToRender = this.props.header;
var footerToRender = this.props.footer;
......@@ -219,6 +245,64 @@ export default class AbstractIncomingWebhook extends React.Component {
</div>
</div>
</div>
{ this.props.enablePostUsernameOverride &&
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='username'
>
<FormattedMessage
id='add_incoming_webhook.username'
defaultMessage='Username'
/>
</label>
<div className='col-md-5 col-sm-8'>
<input
id='username'
type='text'
maxLength='22'
className='form-control'
value={this.state.username}
onChange={this.updateUsername}
/>
<div className='form__help'>
<FormattedMessage
id='add_incoming_webhook.username.help'
defaultMessage='Choose the username this integration will post as. Usernames can be up to 22 characters, and may contain lowercase letters, numbers and the symbols "-", "_", and ".".'
/>
</div>
</div>
</div>
}
{ this.props.enablePostIconOverride &&
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='iconURL'
>
<FormattedMessage
id='add_incoming_webhook.icon_url'
defaultMessage='Profile Picture'
/>
</label>
<div className='col-md-5 col-sm-8'>
<input
id='iconURL'
type='text'
maxLength='1024'
className='form-control'
value={this.state.iconURL}
onChange={this.updateIconURL}
/>
<div className='form__help'>
<FormattedMessage
id='add_incoming_webhook.icon_url.help'
defaultMessage='Choose the profile picture this integration will use when posting. Enter the URL of a .png or .jpg file at least 128 pixels by 128 pixels.'
/>
</div>
</div>
</div>
}
<div className='backstage-form__footer'>
<FormError
type='backstage'
......
......@@ -23,6 +23,16 @@ export default class AddIncomingWebhook extends React.PureComponent {
*/
createIncomingHookRequest: PropTypes.object.isRequired,
/**
* Whether to allow configuration of the default post username.
*/
enablePostUsernameOverride: PropTypes.bool.isRequired,
/**
* Whether to allow configuration of the default post icon.
*/
enablePostIconOverride: PropTypes.bool.isRequired,
actions: PropTypes.shape({
/**
......@@ -60,6 +70,8 @@ export default class AddIncomingWebhook extends React.PureComponent {
team={this.props.team}
header={HEADER}
footer={FOOTER}
enablePostUsernameOverride={this.props.enablePostUsernameOverride}
enablePostIconOverride={this.props.enablePostIconOverride}
action={this.addIncomingHook}
serverError={this.state.serverError}
/>
......
......@@ -9,9 +9,15 @@ import {createIncomingHook} from 'mattermost-redux/actions/integrations';
import AddIncomingWebhook from './add_incoming_webhook.jsx';
function mapStateToProps(state, ownProps) {
const config = state.entities.general.config;
const enablePostUsernameOverride = config.EnablePostUsernameOverride === 'true';
const enablePostIconOverride = config.EnablePostIconOverride === 'true';
return {
...ownProps,
createIncomingHookRequest: state.requests.integrations.createIncomingHook
createIncomingHookRequest: state.requests.integrations.createIncomingHook,
enablePostUsernameOverride,
enablePostIconOverride
};
}
......
......@@ -34,6 +34,21 @@ export default class EditIncomingWebhook extends React.PureComponent {
*/
updateIncomingHookRequest: PropTypes.object.isRequired,
/**
* Whether or not incoming webhooks are enabled.
*/
enableIncomingWebhooks: PropTypes.bool.isRequired,
/**
* Whether to allow configuration of the default post username.
*/
enablePostUsernameOverride: PropTypes.bool.isRequired,
/**
* Whether to allow configuration of the default post icon.
*/
enablePostIconOverride: PropTypes.bool.isRequired,
actions: PropTypes.shape({
/**
......@@ -58,7 +73,7 @@ export default class EditIncomingWebhook extends React.PureComponent {
}
componentDidMount() {
if (window.mm_config.EnableIncomingWebhooks === 'true') {
if (this.props.enableIncomingWebhooks) {
this.props.actions.getIncomingHook(this.props.hookId);
}
}
......@@ -102,6 +117,8 @@ export default class EditIncomingWebhook extends React.PureComponent {
team={this.props.team}
header={HEADER}
footer={FOOTER}
enablePostUsernameOverride={this.props.enablePostUsernameOverride}
enablePostIconOverride={this.props.enablePostIconOverride}
action={this.editIncomingHook}
serverError={this.state.serverError}
initialHook={this.props.hook}
......
......@@ -10,12 +10,19 @@ import EditIncomingWebhook from './edit_incoming_webhook.jsx';
function mapStateToProps(state, ownProps) {
const hookId = ownProps.location.query.id;
const config = state.entities.general.config;
const enableIncomingWebhooks = config.EnableIncomingWebhooks === 'true';
const enablePostUsernameOverride = config.EnablePostUsernameOverride === 'true';
const enablePostIconOverride = config.EnablePostIconOverride === 'true';
return {
...ownProps,
hookId,
hook: state.entities.integrations.incomingHooks[hookId],
updateIncomingHookRequest: state.requests.integrations.createIncomingHook
updateIncomingHookRequest: state.requests.integrations.updateIncomingHook,
enableIncomingWebhooks,
enablePostUsernameOverride,
enablePostIconOverride
};
}
......
......@@ -97,8 +97,12 @@
"add_incoming_webhook.displayName.help": "Choose a title to be displayed on the webhook settings page. Maximum 64 characters.",
"add_incoming_webhook.doneHelp": "Your incoming webhook has been set up. Please send data to the following URL (see <a href=\"https://docs.mattermost.com/developer/webhooks-incoming.html\">documentation</a> for further details).",
"add_incoming_webhook.name": "Name",
"add_incoming_webhook.icon_url": "Profile Picture",
"add_incoming_webhook.icon_url.help": "Choose the profile picture this integration will use when posting. Enter the URL of a .png or .jpg file at least 128 pixels by 128 pixels.",
"add_incoming_webhook.save": "Save",
"add_incoming_webhook.url": "<b>URL</b>: {url}",
"add_incoming_webhook.username": "Username",
"add_incoming_webhook.username.help": "Choose the username this integration will post as. Usernames can be up to 22 characters, and may contain lowercase letters, numbers and the symbols \"-\", \"_\", and \".\" .",
"add_oauth_app.callbackUrls.help": "The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.",
"add_oauth_app.callbackUrlsRequired": "One or more callback URLs are required",
"add_oauth_app.clientId": "<b>Client ID</b>: {id}",
......
......@@ -3,6 +3,8 @@
exports[`components/integrations/AddIncomingWebhook should match snapshot 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Save",
......
......@@ -3,6 +3,8 @@
exports[`components/integrations/EditIncomingWebhook should have called submitHook when editIncomingHook is initiated (no server error) 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Update",
......@@ -34,6 +36,8 @@ exports[`components/integrations/EditIncomingWebhook should have called submitHo
exports[`components/integrations/EditIncomingWebhook should have called submitHook when editIncomingHook is initiated (with data) 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Update",
......@@ -65,6 +69,8 @@ exports[`components/integrations/EditIncomingWebhook should have called submitHo
exports[`components/integrations/EditIncomingWebhook should have called submitHook when editIncomingHook is initiated (with server error) 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Update",
......@@ -102,6 +108,8 @@ exports[`components/integrations/EditIncomingWebhook should not call getIncoming
exports[`components/integrations/EditIncomingWebhook should show AbstractIncomingWebhook 1`] = `
<AbstractIncomingWebhook
action={[Function]}
enablePostIconOverride={true}
enablePostUsernameOverride={true}
footer={
Object {
"defaultMessage": "Update",
......
......@@ -16,6 +16,8 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
channel_id: '88cxd9wpzpbpfp8pad78xj75pr',
description: 'testing'
};
const enablePostUsernameOverride = true;
const enablePostIconOverride = true;
const action = jest.genMockFunction().mockImplementation(
() => {
......@@ -31,6 +33,8 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
footer,
serverError,
initialHook,
enablePostUsernameOverride,
enablePostIconOverride,
action
};
......@@ -59,6 +63,24 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, hiding post username if not enabled', () => {
const props = {
...requiredProps,
enablePostUsernameOverride: false
};
const wrapper = shallow(<AbstractIncomingWebhook {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, hiding post icon url if not enabled', () => {
const props = {
...requiredProps,
enablePostIconOverride: false
};
const wrapper = shallow(<AbstractIncomingWebhook {...props}/>);
expect(wrapper).toMatchSnapshot();
});
test('should call action function', () => {
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
expect(wrapper).toMatchSnapshot();
......@@ -97,4 +119,30 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
expect(wrapper.state('description')).toBe(newDescription);
});
test('should update state.username on post username change', () => {
const newUsername = 'new_username';
const evt = {
preventDefault: jest.fn(),
target: {value: newUsername}
};
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
wrapper.find('#username').simulate('change', evt);
expect(wrapper.state('username')).toBe(newUsername);
});
test('should update state.iconURL on post icon url change', () => {
const newIconURL = 'http://example.com/icon';
const evt = {
preventDefault: jest.fn(),
target: {value: newIconURL}
};
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
wrapper.find('#iconURL').simulate('change', evt);
expect(wrapper.state('iconURL')).toBe(newIconURL);
});
});
......@@ -17,6 +17,8 @@ describe('components/integrations/AddIncomingWebhook', () => {
status: 'not_started',
error: null
},
enablePostUsernameOverride: true,
enablePostIconOverride: true,
actions: {createIncomingHook}
};
......@@ -29,7 +31,9 @@ describe('components/integrations/AddIncomingWebhook', () => {
const hook = {
channel_id: 'channel_id',
display_name: 'display_name',
description: 'description'
description: 'description',
username: 'username',
icon_url: 'icon_url'
};
const wrapper = shallow(<AddIncomingWebhook {...props}/>);
wrapper.instance().addIncomingHook(hook);
......
......@@ -8,8 +8,6 @@ import {browserHistory} from 'react-router';
import EditIncomingWebhook from 'components/integrations/components/edit_incoming_webhook/edit_incoming_webhook.jsx';
describe('components/integrations/EditIncomingWebhook', () => {
global.window.mm_config = {};
const hook = {
id: 'id',
token: 'token'
......@@ -29,20 +27,19 @@ describe('components/integrations/EditIncomingWebhook', () => {
updateIncomingHookRequest: {
status: 'not_started',
error: null
}
},
enableIncomingWebhooks: true,
enablePostUsernameOverride: true,
enablePostIconOverride: true
};
afterEach(() => {
global.window.mm_config = {};
updateIncomingHook = null;
getIncomingHook = null;
actions = {};
});
beforeEach(() => {
global.window.mm_config.EnableIncomingWebhooks = 'true';
updateIncomingHook = jest.fn();
getIncomingHook = jest.fn();
actions = {
......@@ -68,9 +65,7 @@ describe('components/integrations/EditIncomingWebhook', () => {
});
test('should not call getIncomingHook', () => {
global.window.mm_config.EnableIncomingWebhooks = 'false';
const props = {...requiredProps, actions};
const props = {...requiredProps, enableIncomingWebhooks: false, actions};
const wrapper = shallow(<EditIncomingWebhook {...props}/>);
expect(wrapper).toMatchSnapshot();
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment