PLT-6443 Migrate add_oauth_app.jsx to be pure and use Redux (#7232)

* Migrate add_oauth_app.jsx to be pure and use Redux, add tests

* Remove unused flux code for OAuthApps
Этот коммит содержится в:
n1aba
2017-08-18 08:42:10 -05:00
коммит произвёл Harrison Healey
родитель 9097289c2c
Коммит f40f41ed53
6 изменённых файлов: 762 добавлений и 62 удалений

Просмотреть файл

@@ -7,16 +7,6 @@ const getState = store.getState;
import * as IntegrationActions from 'mattermost-redux/actions/integrations';
export function listOAuthApps(complete) {
IntegrationActions.getOAuthApps(0, 10000)(dispatch, getState).then(
(data) => {
if (complete) {
complete(data);
}
}
);
}
export function deleteOAuthApp(id, success, error) {
IntegrationActions.deleteOAuthApp(id)(dispatch, getState).then(
(data) => {
@@ -29,16 +19,3 @@ export function deleteOAuthApp(id, success, error) {
}
);
}
export function registerOAuthApp(app, success, error) {
IntegrationActions.addOAuthApp(app)(dispatch, getState).then(
(data) => {
if (data && success) {
success(data);
} else if (data == null && error) {
const serverError = getState().requests.integrations.addOAuthApp.error;
error({id: serverError.server_error_id, ...serverError});
}
}
);
}

Просмотреть файл

@@ -1,11 +1,8 @@
import PropTypes from 'prop-types';
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import * as OAuthActions from 'actions/oauth_actions.jsx';
import PropTypes from 'prop-types';
import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
import {FormattedMessage} from 'react-intl';
@@ -13,26 +10,31 @@ import FormError from 'components/form_error.jsx';
import {browserHistory, Link} from 'react-router/es6';
import SpinnerButton from 'components/spinner_button.jsx';
export default class AddOAuthApp extends React.Component {
static get propTypes() {
return {
team: PropTypes.object
};
export default class AddOAuthApp extends React.PureComponent {
static propTypes = {
/**
* The team data
*/
team: PropTypes.object,
/**
* The request state for addOAuthApp action. Contains status and error
*/
addOAuthAppRequest: PropTypes.object.isRequired,
actions: PropTypes.shape({
/**
* The function to call to add new OAuthApp
*/
addOAuthApp: PropTypes.func.isRequired
}).isRequired
}
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.updateName = this.updateName.bind(this);
this.updateTrusted = this.updateTrusted.bind(this);
this.updateDescription = this.updateDescription.bind(this);
this.updateHomepage = this.updateHomepage.bind(this);
this.updateIconUrl = this.updateIconUrl.bind(this);
this.updateCallbackUrls = this.updateCallbackUrls.bind(this);
this.imageLoaded = this.imageLoaded.bind(this);
this.image = new Image();
this.image.onload = this.imageLoaded;
@@ -50,14 +52,14 @@ export default class AddOAuthApp extends React.Component {
};
}
imageLoaded() {
imageLoaded = () => {
this.setState({
has_icon: true,
icon_url: this.refs.icon_url.value
});
}
handleSubmit(e) {
handleSubmit = (e) => {
e.preventDefault();
if (this.state.saving) {
@@ -144,45 +146,47 @@ export default class AddOAuthApp extends React.Component {
icon_url: this.state.icon_url
};
OAuthActions.registerOAuthApp(
app,
this.props.actions.addOAuthApp(app).then(
(data) => {
browserHistory.push('/' + this.props.team.name + '/integrations/confirm?type=oauth2-apps&id=' + data.id);
},
(err) => {
this.setState({
saving: false,
serverError: err.message
});
const {error} = this.props.addOAuthAppRequest;
if (error) {
this.setState({
saving: false,
serverError: error.message
});
} else {
browserHistory.
push(`/${this.props.team.name}/integrations/confirm?type=oauth2-apps&id=${data.id}`);
}
}
);
}
updateName(e) {
updateName = (e) => {
this.setState({
name: e.target.value
});
}
updateTrusted(e) {
updateTrusted = (e) => {
this.setState({
is_trusted: e.target.value === 'true'
});
}
updateDescription(e) {
updateDescription = (e) => {
this.setState({
description: e.target.value
});
}
updateHomepage(e) {
updateHomepage = (e) => {
this.setState({
homepage: e.target.value
});
}
updateIconUrl(e) {
updateIconUrl = (e) => {
this.setState({
has_icon: false,
icon_url: ''
@@ -190,7 +194,7 @@ export default class AddOAuthApp extends React.Component {
this.image.src = e.target.value;
}
updateCallbackUrls(e) {
updateCallbackUrls = (e) => {
this.setState({
callbackUrls: e.target.value
});
@@ -209,7 +213,7 @@ export default class AddOAuthApp extends React.Component {
return (
<div className='backstage-content'>
<BackstageHeader>
<Link to={'/' + this.props.team.name + '/integrations/oauth2-apps'}>
<Link to={`/${this.props.team.name}/integrations/oauth2-apps`}>
<FormattedMessage
id='installed_oauth_apps.header'
defaultMessage='Installed OAuth2 Apps'
@@ -410,7 +414,7 @@ export default class AddOAuthApp extends React.Component {
/>
<Link
className='btn btn-sm'
to={'/' + this.props.team.name + '/integrations/oauth2-apps'}
to={`/${this.props.team.name}/integrations/oauth2-apps`}
>
<FormattedMessage
id='installed_oauth_apps.cancel'

Просмотреть файл

@@ -0,0 +1,25 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {addOAuthApp} from 'mattermost-redux/actions/integrations';
import AddOAuthApp from './add_oauth_app.jsx';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
addOAuthAppRequest: state.requests.integrations.addOAuthApp
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
addOAuthApp
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AddOAuthApp);

Просмотреть файл

@@ -102,7 +102,7 @@ export default {
{
path: 'add',
getComponents: (location, callback) => {
System.import('components/integrations/components/add_oauth_app.jsx').then(RouteUtils.importComponentSuccess(callback));
System.import('components/integrations/components/add_oauth_app').then(RouteUtils.importComponentSuccess(callback));
}
}
]

Просмотреть файл

@@ -0,0 +1,613 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/integrations/AddOAuthApp should match snapshot 1`] = `
<div
className="backstage-content"
>
<BackstageHeader>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/test/integrations/oauth2-apps"
>
<FormattedMessage
defaultMessage="Installed OAuth2 Apps"
id="installed_oauth_apps.header"
values={Object {}}
/>
</Link>
<FormattedMessage
defaultMessage="Add"
id="add_oauth_app.header"
values={Object {}}
/>
</BackstageHeader>
<div
className="backstage-form"
>
<form
className="form-horizontal"
>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="is_trusted"
>
<FormattedMessage
defaultMessage="Is Trusted"
id="installed_oauth_apps.trusted"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<label
className="radio-inline"
>
<input
checked={false}
name="is_trusted"
onChange={[Function]}
type="radio"
value="true"
/>
<FormattedMessage
defaultMessage="Yes"
id="installed_oauth_apps.trusted.yes"
values={Object {}}
/>
</label>
<label
className="radio-inline"
>
<input
checked={true}
name="is_trusted"
onChange={[Function]}
type="radio"
value="false"
/>
<FormattedMessage
defaultMessage="No"
id="installed_oauth_apps.trusted.no"
values={Object {}}
/>
</label>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="When true, the OAuth 2.0 application is considered trusted by the Mattermost server and doesn't require the user to accept authorization. When false, an additional window will appear, asking the user to accept or deny the authorization."
id="add_oauth_app.trusted.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="name"
>
<FormattedMessage
defaultMessage="Display Name"
id="installed_oauth_apps.name"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="name"
maxLength="64"
onChange={[Function]}
type="text"
value=""
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="Display name for your OAuth 2.0 application made of up to 64 characters."
id="add_oauth_app.name.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="description"
>
<FormattedMessage
defaultMessage="Description"
id="installed_oauth_apps.description"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="description"
maxLength="512"
onChange={[Function]}
type="text"
value=""
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="Description for your OAuth 2.0 application."
id="add_oauth_app.description.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="homepage"
>
<FormattedMessage
defaultMessage="Homepage"
id="installed_oauth_apps.homepage"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="homepage"
maxLength="256"
onChange={[Function]}
type="url"
value=""
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration."
id="add_oauth_app.homepage.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="icon_url"
>
<FormattedMessage
defaultMessage="Icon URL"
id="installed_oauth_apps.iconUrl"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="icon_url"
maxLength="512"
onChange={[Function]}
type="url"
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration."
id="add_oauth_app.icon.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="callbackUrls"
>
<FormattedMessage
defaultMessage="Callback URLs (One Per Line)"
id="installed_oauth_apps.callbackUrls"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<textarea
className="form-control"
id="callbackUrls"
maxLength="1024"
onChange={[Function]}
rows="3"
value=""
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="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://."
id="add_oauth_app.callbackUrls.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="backstage-form__footer"
>
<FormError
error={null}
errors={
Array [
"",
null,
]
}
type="backstage"
/>
<Link
className="btn btn-sm"
onlyActiveOnIndex={false}
style={Object {}}
to="/test/integrations/oauth2-apps"
>
<FormattedMessage
defaultMessage="Cancel"
id="installed_oauth_apps.cancel"
values={Object {}}
/>
</Link>
<SpinnerButton
className="btn btn-primary"
onClick={[Function]}
spinning={false}
type="submit"
>
<FormattedMessage
defaultMessage="Save"
id="installed_oauth_apps.save"
values={Object {}}
/>
</SpinnerButton>
</div>
</form>
</div>
</div>
`;
exports[`components/integrations/AddOAuthApp should match snapshot, displays client error 1`] = `
<div
className="backstage-content"
>
<BackstageHeader>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/test/integrations/oauth2-apps"
>
<FormattedMessage
defaultMessage="Installed OAuth2 Apps"
id="installed_oauth_apps.header"
values={Object {}}
/>
</Link>
<FormattedMessage
defaultMessage="Add"
id="add_oauth_app.header"
values={Object {}}
/>
</BackstageHeader>
<div
className="backstage-form"
>
<form
className="form-horizontal"
>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="is_trusted"
>
<FormattedMessage
defaultMessage="Is Trusted"
id="installed_oauth_apps.trusted"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<label
className="radio-inline"
>
<input
checked={false}
name="is_trusted"
onChange={[Function]}
type="radio"
value="true"
/>
<FormattedMessage
defaultMessage="Yes"
id="installed_oauth_apps.trusted.yes"
values={Object {}}
/>
</label>
<label
className="radio-inline"
>
<input
checked={true}
name="is_trusted"
onChange={[Function]}
type="radio"
value="false"
/>
<FormattedMessage
defaultMessage="No"
id="installed_oauth_apps.trusted.no"
values={Object {}}
/>
</label>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="When true, the OAuth 2.0 application is considered trusted by the Mattermost server and doesn't require the user to accept authorization. When false, an additional window will appear, asking the user to accept or deny the authorization."
id="add_oauth_app.trusted.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="name"
>
<FormattedMessage
defaultMessage="Display Name"
id="installed_oauth_apps.name"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="name"
maxLength="64"
onChange={[Function]}
type="text"
value=""
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="Display name for your OAuth 2.0 application made of up to 64 characters."
id="add_oauth_app.name.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="description"
>
<FormattedMessage
defaultMessage="Description"
id="installed_oauth_apps.description"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="description"
maxLength="512"
onChange={[Function]}
type="text"
value=""
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="Description for your OAuth 2.0 application."
id="add_oauth_app.description.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="homepage"
>
<FormattedMessage
defaultMessage="Homepage"
id="installed_oauth_apps.homepage"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="homepage"
maxLength="256"
onChange={[Function]}
type="url"
value=""
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration."
id="add_oauth_app.homepage.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="icon_url"
>
<FormattedMessage
defaultMessage="Icon URL"
id="installed_oauth_apps.iconUrl"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<input
className="form-control"
id="icon_url"
maxLength="512"
onChange={[Function]}
type="url"
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration."
id="add_oauth_app.icon.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="form-group"
>
<label
className="control-label col-sm-4"
htmlFor="callbackUrls"
>
<FormattedMessage
defaultMessage="Callback URLs (One Per Line)"
id="installed_oauth_apps.callbackUrls"
values={Object {}}
/>
</label>
<div
className="col-md-5 col-sm-8"
>
<textarea
className="form-control"
id="callbackUrls"
maxLength="1024"
onChange={[Function]}
rows="3"
value=""
/>
<div
className="form__help"
>
<FormattedMessage
defaultMessage="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://."
id="add_oauth_app.callbackUrls.help"
values={Object {}}
/>
</div>
</div>
</div>
<div
className="backstage-form__footer"
>
<FormError
error={null}
errors={
Array [
"",
<FormattedMessage
defaultMessage="Name for the OAuth 2.0 application is required."
id="add_oauth_app.nameRequired"
values={Object {}}
/>,
]
}
type="backstage"
/>
<Link
className="btn btn-sm"
onlyActiveOnIndex={false}
style={Object {}}
to="/test/integrations/oauth2-apps"
>
<FormattedMessage
defaultMessage="Cancel"
id="installed_oauth_apps.cancel"
values={Object {}}
/>
</Link>
<SpinnerButton
className="btn btn-primary"
onClick={[Function]}
spinning={false}
type="submit"
>
<FormattedMessage
defaultMessage="Save"
id="installed_oauth_apps.save"
values={Object {}}
/>
</SpinnerButton>
</div>
</form>
</div>
</div>
`;

Просмотреть файл

@@ -0,0 +1,81 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import AddOAuthApp from 'components/integrations/components/add_oauth_app/add_oauth_app.jsx';
describe('components/integrations/AddOAuthApp', () => {
const emptyFunction = jest.fn();
const team = {
id: 'dbcxd9wpzpbpfp8pad78xj12pr',
name: 'test'
};
test('should match snapshot', () => {
const wrapper = shallow(
<AddOAuthApp
team={team}
addOAuthAppRequest={{
status: 'not_started',
error: null
}}
actions={{addOAuthApp: emptyFunction}}
/>
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot, displays client error', () => {
const wrapper = shallow(
<AddOAuthApp
team={team}
addOAuthAppRequest={{
status: 'not_started',
error: null
}}
actions={{addOAuthApp: emptyFunction}}
/>
);
wrapper.find('.btn-primary').simulate('click', {preventDefault() {
return jest.fn();
}});
expect(wrapper).toMatchSnapshot();
});
test('should call addOAuthApp function', () => {
const addOAuthApp = jest.genMockFunction().mockImplementation(
() => {
return new Promise((resolve) => {
process.nextTick(() => resolve());
});
}
);
const wrapper = shallow(
<AddOAuthApp
team={team}
addOAuthAppRequest={{
status: 'not_started',
error: null
}}
actions={{addOAuthApp}}
/>
);
wrapper.find('#name').simulate('change', {target: {value: 'name'}});
wrapper.find('#description').simulate('change', {target: {value: 'description'}});
wrapper.find('#homepage').simulate('change', {target: {value: 'http://test.com'}});
wrapper.find('#callbackUrls').simulate('change', {target: {value: 'http://callback.com'}});
wrapper.find('.btn-primary').simulate('click', {preventDefault() {
return jest.fn();
}});
expect(addOAuthApp).toBeCalled();
});
});