Skip to content

Commit ec22c7d

Browse files
authored
Merge pull request #5660 from getsentry/ui/more-form-improvements
ui: various improvements to forms
2 parents 8bc8a5a + db08157 commit ec22c7d

19 files changed

+397
-379
lines changed

src/sentry/static/sentry/app/components/forms/apiForm.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,12 @@ export default class ApiForm extends Form {
4343
method: this.props.apiMethod,
4444
data: data,
4545
success: result => {
46+
IndicatorStore.remove(loadingIndicator);
4647
this.onSubmitSuccess(result);
4748
},
4849
error: error => {
49-
this.onSubmitError(error);
50-
},
51-
complete: () => {
5250
IndicatorStore.remove(loadingIndicator);
51+
this.onSubmitError(error);
5352
}
5453
});
5554
}

src/sentry/static/sentry/app/components/forms/form.jsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,26 @@ import {t} from '../../locale';
66

77
export default class Form extends React.Component {
88
static propTypes = {
9+
cancelLabel: React.PropTypes.string,
10+
onCancel: React.PropTypes.func,
911
onSubmit: React.PropTypes.func.isRequired,
1012
onSubmitSuccess: React.PropTypes.func,
1113
onSubmitError: React.PropTypes.func,
1214
submitDisabled: React.PropTypes.bool,
1315
submitLabel: React.PropTypes.string,
1416
footerClass: React.PropTypes.string,
1517
extraButton: React.PropTypes.element,
16-
initialData: React.PropTypes.object
18+
initialData: React.PropTypes.object,
19+
requireChanges: React.PropTypes.bool
1720
};
1821

1922
static defaultProps = {
23+
cancelLabel: t('Cancel'),
2024
submitLabel: t('Save Changes'),
2125
submitDisabled: false,
2226
footerClass: 'form-actions align-right',
23-
className: 'form-stacked'
27+
className: 'form-stacked',
28+
requireChanges: false
2429
};
2530

2631
static childContextTypes = {
@@ -88,7 +93,10 @@ export default class Form extends React.Component {
8893
render() {
8994
let isSaving = this.state.state === FormState.SAVING;
9095
let {initialData, data} = this.state;
91-
let hasChanges = Object.keys(data).length && !underscore.isEqual(data, initialData);
96+
let {requireChanges} = this.props;
97+
let hasChanges = requireChanges
98+
? Object.keys(data).length && !underscore.isEqual(data, initialData)
99+
: true;
92100
return (
93101
<form onSubmit={this.onSubmit} className={this.props.className}>
94102
{this.state.state === FormState.ERROR &&
@@ -105,6 +113,14 @@ export default class Form extends React.Component {
105113
type="submit">
106114
{this.props.submitLabel}
107115
</button>
116+
{this.props.onCancel &&
117+
<button
118+
className="btn btn-default"
119+
disabled={isSaving}
120+
onClick={this.props.onCancel}
121+
style={{marginLeft: 5}}>
122+
{this.props.cancelLabel}
123+
</button>}
108124
{this.props.extraButton}
109125
</div>
110126
</form>

src/sentry/static/sentry/app/components/forms/passwordField.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import InputField from './inputField';
3-
import {FormState} from './state';
3+
import FormState from './state';
44

55
// TODO(dcramer): im not entirely sure this is working correctly with
66
// value propagation in all scenarios

src/sentry/static/sentry/app/options.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ function optionsForSection(section) {
150150
return definitions.filter(option => option.key.split('.')[0] === section.key);
151151
}
152152

153-
export function getOptionField(option, onChange, value, field) {
153+
export function getOptionField(option, field, value, onChange) {
154154
let meta = {...getOption(option), ...field};
155155
let Field = meta.component || TextField;
156156
return (
Lines changed: 34 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import React from 'react';
22
import _ from 'underscore';
33

4-
import AlertActions from '../actions/alertActions';
5-
import ApiMixin from '../mixins/apiMixin';
6-
import IndicatorStore from '../stores/indicatorStore';
7-
import LoadingIndicator from '../components/loadingIndicator';
4+
import AsyncView from './asyncView';
85
import {t} from '../locale';
96
import {getOption, getOptionField} from '../options';
10-
import {Form} from '../components/forms';
7+
import {ApiForm} from '../components/forms';
118

129
const optionsAvailable = [
1310
'system.url-prefix',
@@ -21,190 +18,52 @@ const optionsAvailable = [
2118
'api.rate-limit.org-create'
2219
];
2320

24-
const SettingsList = React.createClass({
25-
propTypes: {
26-
formDisabled: React.PropTypes.bool,
27-
options: React.PropTypes.object.isRequired,
28-
onSubmit: React.PropTypes.func.isRequired
29-
},
21+
export default class AdminSettings extends AsyncView {
22+
getEndpoint() {
23+
return '/internal/options/';
24+
}
25+
26+
renderBody() {
27+
let {data} = this.state;
3028

31-
getInitialState() {
32-
let options = this.props.options;
33-
let formData = {};
34-
let required = [];
29+
let initialData = {};
3530
let fields = {};
3631
for (let key of optionsAvailable) {
3732
// TODO(dcramer): we should not be mutating options
38-
let option = options[key] || {field: {}};
33+
let option = data[key] || {field: {}};
3934
if (_.isUndefined(option.value) || option.value === '') {
4035
let defn = getOption(key);
41-
formData[key] = defn.defaultValue ? defn.defaultValue() : '';
36+
initialData[key] = defn.defaultValue ? defn.defaultValue() : '';
4237
} else {
43-
formData[key] = option.value;
44-
}
45-
if (option.field.required) {
46-
required.push(key);
38+
initialData[key] = option.value;
4739
}
48-
fields[key] = getOptionField(
49-
key,
50-
this.onFieldChange.bind(this, key),
51-
formData[key],
52-
option.field
53-
);
40+
fields[key] = getOptionField(key, option.field);
5441
}
5542

56-
return {
57-
required: required,
58-
formData: formData,
59-
fields: fields
60-
};
61-
},
62-
63-
onFieldChange(name, value) {
64-
let formData = this.state.formData;
65-
formData[name] = value;
66-
this.setState({
67-
formData: formData
68-
});
69-
},
70-
71-
onSubmit(e) {
72-
this.props.onSubmit(this.state.formData);
73-
},
74-
75-
render() {
76-
let {fields, required, formData} = this.state;
77-
let formValid = !required.filter(option => !formData[option]).length;
78-
let submitDisabled = !formValid || this.props.formDisabled;
79-
80-
return (
81-
<Form onSubmit={this.onSubmit} submitDisabled={submitDisabled}>
82-
<h4>General</h4>
83-
{fields['system.url-prefix']}
84-
{fields['system.admin-email']}
85-
{fields['system.support-email']}
86-
{fields['system.security-email']}
87-
{fields['system.rate-limit']}
88-
89-
<h4>Security & Abuse</h4>
90-
{fields['auth.allow-registration']}
91-
{fields['auth.ip-rate-limit']}
92-
{fields['auth.user-rate-limit']}
93-
{fields['api.rate-limit.org-create']}
94-
</Form>
95-
);
96-
}
97-
});
98-
99-
const AdminSettings = React.createClass({
100-
mixins: [ApiMixin],
101-
102-
getInitialState() {
103-
return {
104-
loading: true,
105-
error: false,
106-
submitInProgress: false,
107-
submitError: null,
108-
options: {}
109-
};
110-
},
111-
112-
componentWillMount() {
113-
this.fetchData();
114-
},
115-
116-
remountComponent() {
117-
this.setState(this.getInitialState(), this.fetchData);
118-
},
119-
120-
fetchData(callback) {
121-
this.api.request('/internal/options/', {
122-
method: 'GET',
123-
success: data => {
124-
this.setState({
125-
options: data,
126-
loading: false,
127-
error: false
128-
});
129-
},
130-
error: () => {
131-
this.setState({
132-
loading: false,
133-
error: true
134-
});
135-
}
136-
});
137-
},
138-
139-
onSubmit(formData) {
140-
this.setState({
141-
submitInProgress: true,
142-
submitError: false
143-
});
144-
let loadingIndicator = IndicatorStore.add(t('Saving changes..'));
145-
146-
// We only want to send back the values which weren't disabled
147-
formData = _.pick(formData, (value, key) => {
148-
return !this.state.options[key].field.disabled;
149-
});
150-
this.api.request('/internal/options/', {
151-
method: 'PUT',
152-
data: formData,
153-
success: () => {
154-
this.setState({
155-
submitInProgress: false
156-
});
157-
AlertActions.addAlert({
158-
message: t('Your changes were saved, and will propagate to services shortly.'),
159-
type: 'success'
160-
});
161-
},
162-
error: () => {
163-
this.setState({
164-
submitInProgress: false,
165-
submitError: true
166-
});
167-
},
168-
complete: () => {
169-
IndicatorStore.remove(loadingIndicator);
170-
}
171-
});
172-
},
173-
174-
render() {
175-
let {error, loading, options, submitError, submitInProgress} = this.state;
176-
17743
return (
17844
<div>
17945
<h3>{t('Settings')}</h3>
18046

181-
{loading
182-
? <LoadingIndicator>
183-
{t('Please wait while we load configuration.')}
184-
</LoadingIndicator>
185-
: error
186-
? <div className="loading-error">
187-
<span className="icon" />
188-
{t(
189-
'We were unable to load the required configuration from the Sentry server. Please take a look at the service logs.'
190-
)}
191-
</div>
192-
: <div>
193-
{submitError &&
194-
<div className="alert alert-block alert-error">
195-
{t(
196-
'We were unable to submit your changes to the Sentry server. Please take a look at the service logs.'
197-
)}
198-
</div>}
199-
<SettingsList
200-
options={options}
201-
onSubmit={this.onSubmit}
202-
formDisabled={submitInProgress}
203-
/>
204-
</div>}
47+
<ApiForm
48+
apiMethod="PUT"
49+
apiEndpoint={this.getEndpoint()}
50+
onSubmit={this.onSubmit}
51+
initialData={initialData}
52+
requireChanges={true}>
53+
<h4>General</h4>
54+
{fields['system.url-prefix']}
55+
{fields['system.admin-email']}
56+
{fields['system.support-email']}
57+
{fields['system.security-email']}
58+
{fields['system.rate-limit']}
59+
60+
<h4>Security & Abuse</h4>
61+
{fields['auth.allow-registration']}
62+
{fields['auth.ip-rate-limit']}
63+
{fields['auth.user-rate-limit']}
64+
{fields['api.rate-limit.org-create']}
65+
</ApiForm>
20566
</div>
20667
);
20768
}
208-
});
209-
210-
export default AdminSettings;
69+
}

0 commit comments

Comments
 (0)