diff options
author | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-05-30 16:40:23 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-05-30 16:40:23 +0200 |
commit | 862b59c20a2012f84acc4abf7531cb2fa2b1f3ec (patch) | |
tree | 880652733976f7dbaf074d70d97f1a373e2805d2 /server/sonar-web/src/main/js/apps | |
parent | 88b3c97acd9154a1dc16bd1e158a02f2020ca6c8 (diff) | |
download | sonarqube-862b59c20a2012f84acc4abf7531cb2fa2b1f3ec.tar.gz sonarqube-862b59c20a2012f84acc4abf7531cb2fa2b1f3ec.zip |
rework quality profiles modals (#2123)
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
33 files changed, 1147 insertions, 657 deletions
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js index 564996b4c5c..e60087cd0fc 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/App.js @@ -30,6 +30,7 @@ type Props = { children: React.Element<*>, currentUser: { permissions: { global: Array<string> } }, languages: Array<*>, + onRequestFail: Object => void, organization: { name: string, canAdmin?: boolean, key: string } | null }; @@ -108,6 +109,7 @@ export default class App extends React.PureComponent { languages: finalLanguages, exporters: this.state.exporters, updateProfiles: this.updateProfiles, + onRequestFail: this.props.onRequestFail, organization: organization ? organization.key : null, canAdmin }); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.js index b5ff40a9ace..a9809ffe0dc 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/AppContainer.js @@ -20,6 +20,7 @@ import { connect } from 'react-redux'; import App from './App'; import { getLanguages, getCurrentUser, getOrganizationByKey } from '../../../store/rootReducer'; +import { onFail } from '../../../store/rootActions'; const mapStateToProps = (state, ownProps) => ({ currentUser: getCurrentUser(state), @@ -29,4 +30,8 @@ const mapStateToProps = (state, ownProps) => ({ : null }); -export default connect(mapStateToProps)(App); +const mapDispatchToProps = dispatch => ({ + onRequestFail: error => onFail(dispatch)(error) +}); + +export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.js new file mode 100644 index 00000000000..3ced39bc3e3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.js @@ -0,0 +1,134 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import type { Profile } from '../propTypes'; +import { copyProfile } from '../../../api/quality-profiles'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +type Props = { + onClose: () => void, + onCopy: string => void, + onRequestFail: Object => void, + profile: Profile +}; + +type State = { + loading: boolean, + name: ?string +}; + +export default class CopyProfileForm extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { loading: false, name: null }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleNameChange = (event: { currentTarget: HTMLInputElement }) => { + this.setState({ name: event.currentTarget.value }); + }; + + handleFormSubmit = (event: Event) => { + event.preventDefault(); + + const { name } = this.state; + + if (name != null) { + this.setState({ loading: true }); + copyProfile(this.props.profile.key, name).then( + profile => this.props.onCopy(profile.name), + error => { + if (this.mounted) { + this.setState({ loading: false }); + } + this.props.onRequestFail(error); + } + ); + } + }; + + render() { + const { profile } = this.props; + const header = translateWithParameters( + 'quality_profiles.copy_x_title', + profile.name, + profile.languageName + ); + const submitDisabled = + this.state.loading || !this.state.name || this.state.name === profile.name; + + return ( + <Modal + isOpen={true} + contentLabel={header} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <form id="copy-profile-form" onSubmit={this.handleFormSubmit}> + <div className="modal-head"> + <h2>{header}</h2> + </div> + <div className="modal-body"> + <div className="modal-field"> + <label htmlFor="copy-profile-name"> + {translate('quality_profiles.copy_new_name')}<em className="mandatory">*</em> + </label> + <input + autoFocus={true} + id="copy-profile-name" + maxLength="100" + name="name" + onChange={this.handleNameChange} + required={true} + size="50" + type="text" + value={this.state.name != null ? this.state.name : profile.name} + /> + </div> + </div> + <div className="modal-foot"> + {this.state.loading && <i className="spinner spacer-right" />} + <button disabled={submitDisabled} id="copy-profile-submit"> + {translate('copy')} + </button> + <a href="#" id="copy-profile-cancel" onClick={this.handleCancelClick}> + {translate('cancel')} + </a> + </div> + </form> + + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.js new file mode 100644 index 00000000000..03ca6da00cd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.js @@ -0,0 +1,120 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import type { Profile } from '../propTypes'; +import { deleteProfile } from '../../../api/quality-profiles'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +type Props = { + onClose: () => void, + onDelete: () => void, + onRequestFail: Object => void, + profile: Profile +}; + +type State = { + loading: boolean +}; + +export default class DeleteProfileForm extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { loading: false, name: null }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleFormSubmit = (event: Event) => { + event.preventDefault(); + this.setState({ loading: true }); + deleteProfile(this.props.profile.key).then(this.props.onDelete, error => { + if (this.mounted) { + this.setState({ loading: false }); + } + this.props.onRequestFail(error); + }); + }; + + render() { + const { profile } = this.props; + const header = translate('quality_profiles.delete_confirm_title'); + + return ( + <Modal + isOpen={true} + contentLabel={header} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <form id="delete-profile-form" onSubmit={this.handleFormSubmit}> + <div className="modal-head"> + <h2>{header}</h2> + </div> + <div className="modal-body"> + <div className="js-modal-messages" /> + {profile.childrenCount > 0 + ? <div> + <div className="alert alert-warning"> + {translate('quality_profiles.this_profile_has_descendants')} + </div> + <p> + {translateWithParameters( + 'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants', + profile.name, + profile.languageName + )} + </p> + </div> + : <p> + {translateWithParameters( + 'quality_profiles.are_you_sure_want_delete_profile_x', + profile.name, + profile.languageName + )} + </p>} + </div> + <div className="modal-foot"> + {this.state.loading && <i className="spinner spacer-right" />} + <button className="button-red" disabled={this.state.loading} id="delete-profile-submit"> + {translate('delete')} + </button> + <a href="#" id="delete-profile-cancel" onClick={this.handleCancelClick}> + {translate('cancel')} + </a> + </div> + </form> + + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js index ec8f51ee0a0..af02d8d9539 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.js @@ -20,9 +20,9 @@ // @flow import React from 'react'; import { Link } from 'react-router'; -import RenameProfileView from '../views/RenameProfileView'; -import CopyProfileView from '../views/CopyProfileView'; -import DeleteProfileView from '../views/DeleteProfileView'; +import RenameProfileForm from './RenameProfileForm'; +import CopyProfileForm from './CopyProfileForm'; +import DeleteProfileForm from './DeleteProfileForm'; import { translate } from '../../../helpers/l10n'; import { getRulesUrl } from '../../../helpers/urls'; import { setDefaultProfile } from '../../../api/quality-profiles'; @@ -32,13 +32,21 @@ import type { Profile } from '../propTypes'; type Props = { canAdmin: boolean, fromList: boolean, + onRequestFail: Object => void, organization: ?string, profile: Profile, updateProfiles: () => Promise<*> }; +type State = { + copyFormOpen: boolean, + deleteFormOpen: boolean, + renameFormOpen: boolean +}; + export default class ProfileActions extends React.PureComponent { props: Props; + state: State; static defaultProps = { fromList: false @@ -48,32 +56,50 @@ export default class ProfileActions extends React.PureComponent { router: React.PropTypes.object }; - handleRenameClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - new RenameProfileView({ profile: this.props.profile }) - .on('done', (newName: string) => { - this.props.updateProfiles().then(() => { - if (!this.props.fromList) { - this.context.router.replace( - getProfilePath(newName, this.props.profile.language, this.props.organization) - ); - } - }); - }) - .render(); + constructor(props: Props) { + super(props); + this.state = { + copyFormOpen: false, + deleteFormOpen: false, + renameFormOpen: false + }; + } + + handleRenameClick = (event: Event) => { + event.preventDefault(); + this.setState({ renameFormOpen: true }); }; - handleCopyClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - new CopyProfileView({ profile: this.props.profile }) - .on('done', profile => { - this.props.updateProfiles().then(() => { - this.context.router.push( - getProfilePath(profile.name, profile.language, this.props.organization) - ); - }); - }) - .render(); + handleProfileRename = (name: string) => { + this.closeRenameForm(); + this.props.updateProfiles().then(() => { + if (!this.props.fromList) { + this.context.router.replace( + getProfilePath(name, this.props.profile.language, this.props.organization) + ); + } + }); + }; + + closeRenameForm = () => { + this.setState({ renameFormOpen: false }); + }; + + handleCopyClick = (event: Event) => { + event.preventDefault(); + this.setState({ copyFormOpen: true }); + }; + + handleProfileCopy = (name: string) => { + this.props.updateProfiles().then(() => { + this.context.router.push( + getProfilePath(name, this.props.profile.language, this.props.organization) + ); + }); + }; + + closeCopyForm = () => { + this.setState({ copyFormOpen: false }); }; handleSetDefaultClick = (e: SyntheticInputEvent) => { @@ -81,14 +107,18 @@ export default class ProfileActions extends React.PureComponent { setDefaultProfile(this.props.profile.key).then(this.props.updateProfiles); }; - handleDeleteClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - new DeleteProfileView({ profile: this.props.profile }) - .on('done', () => { - this.context.router.replace(getProfilesPath(this.props.organization)); - this.props.updateProfiles(); - }) - .render(); + handleDeleteClick = (event: Event) => { + event.preventDefault(); + this.setState({ deleteFormOpen: true }); + }; + + handleProfileDelete = () => { + this.context.router.replace(getProfilesPath(this.props.organization)); + this.props.updateProfiles(); + }; + + closeDeleteForm = () => { + this.setState({ deleteFormOpen: false }); }; render() { @@ -152,6 +182,30 @@ export default class ProfileActions extends React.PureComponent { {translate('delete')} </a> </li>} + + {this.state.copyFormOpen && + <CopyProfileForm + onClose={this.closeCopyForm} + onCopy={this.handleProfileCopy} + onRequestFail={this.props.onRequestFail} + profile={profile} + />} + + {this.state.deleteFormOpen && + <DeleteProfileForm + onClose={this.closeDeleteForm} + onDelete={this.handleProfileDelete} + onRequestFail={this.props.onRequestFail} + profile={profile} + />} + + {this.state.renameFormOpen && + <RenameProfileForm + onClose={this.closeRenameForm} + onRename={this.handleProfileRename} + onRequestFail={this.props.onRequestFail} + profile={profile} + />} </ul> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js index ed1d683ed1c..ceda3d91f5f 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileContainer.js @@ -31,6 +31,7 @@ type Props = { pathname: string, query: { key?: string, language: string, name: string } }, + onRequestFail: Object => void, organization: ?string, profiles: Array<Profile>, router: { replace: () => void }, @@ -78,6 +79,7 @@ export default class ProfileContainer extends React.PureComponent { } const child = React.cloneElement(this.props.children, { + onRequestFail: this.props.onRequestFail, organization, profile, profiles, @@ -89,6 +91,7 @@ export default class ProfileContainer extends React.PureComponent { <Helmet title={profile.name} /> <ProfileHeader canAdmin={this.props.canAdmin} + onRequestFail={this.props.onRequestFail} organization={organization} profile={profile} updateProfiles={this.props.updateProfiles} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.js b/server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.js new file mode 100644 index 00000000000..9a2af870205 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.js @@ -0,0 +1,134 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import type { Profile } from '../propTypes'; +import { renameProfile } from '../../../api/quality-profiles'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +type Props = { + onClose: () => void, + onRename: string => void, + onRequestFail: Object => void, + profile: Profile +}; + +type State = { + loading: boolean, + name: ?string +}; + +export default class RenameProfileForm extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { loading: false, name: null }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleNameChange = (event: { currentTarget: HTMLInputElement }) => { + this.setState({ name: event.currentTarget.value }); + }; + + handleFormSubmit = (event: Event) => { + event.preventDefault(); + + const { name } = this.state; + + if (name != null) { + this.setState({ loading: true }); + renameProfile(this.props.profile.key, name).then( + () => this.props.onRename(name), + error => { + if (this.mounted) { + this.setState({ loading: false }); + } + this.props.onRequestFail(error); + } + ); + } + }; + + render() { + const { profile } = this.props; + const header = translateWithParameters( + 'quality_profiles.rename_x_title', + profile.name, + profile.languageName + ); + const submitDisabled = + this.state.loading || !this.state.name || this.state.name === profile.name; + + return ( + <Modal + isOpen={true} + contentLabel={header} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <form id="rename-profile-form" onSubmit={this.handleFormSubmit}> + <div className="modal-head"> + <h2>{header}</h2> + </div> + <div className="modal-body"> + <div className="modal-field"> + <label htmlFor="rename-profile-name"> + {translate('quality_profiles.new_name')}<em className="mandatory">*</em> + </label> + <input + autoFocus={true} + id="rename-profile-name" + maxLength="100" + name="name" + onChange={this.handleNameChange} + required={true} + size="50" + type="text" + value={this.state.name != null ? this.state.name : profile.name} + /> + </div> + </div> + <div className="modal-foot"> + {this.state.loading && <i className="spinner spacer-right" />} + <button disabled={submitDisabled} id="rename-profile-submit"> + {translate('rename')} + </button> + <a href="#" id="rename-profile-cancel" onClick={this.handleCancelClick}> + {translate('cancel')} + </a> + </div> + </form> + + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeParentForm.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeParentForm.js new file mode 100644 index 00000000000..5f0aeb88690 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeParentForm.js @@ -0,0 +1,141 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import Select from 'react-select'; +import { sortBy } from 'lodash'; +import { changeProfileParent } from '../../../api/quality-profiles'; +import { translate } from '../../../helpers/l10n'; +import type { Profile } from '../propTypes'; + +type Props = { + onChange: () => void, + onClose: () => void, + onRequestFail: Object => void, + profile: Profile, + profiles: Array<Profile> +}; + +type State = { + loading: boolean, + selected: ?string +}; + +export default class ChangeParentForm extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { + loading: false, + selected: null + }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleSelectChange = (option: { value: string }) => { + this.setState({ selected: option.value }); + }; + + handleFormSubmit = (event: Event) => { + event.preventDefault(); + + const parent = this.state.selected; + + if (parent != null) { + this.setState({ loading: true }); + changeProfileParent(this.props.profile.key, parent).then(this.props.onChange).catch(error => { + if (this.mounted) { + this.setState({ loading: false }); + } + this.props.onRequestFail(error); + }); + } + }; + + render() { + const { profiles } = this.props; + + const options = [ + { label: translate('none'), value: '' }, + ...sortBy(profiles, 'name').map(profile => ({ label: profile.name, value: profile.key })) + ]; + + const submitDisabled = + this.state.loading || + this.state.selected == null || + this.state.selected === this.props.profile.parentKey; + + return ( + <Modal + isOpen={true} + contentLabel={translate('quality_profiles.change_parent')} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <form id="change-profile-parent-form" onSubmit={this.handleFormSubmit}> + <div className="modal-head"> + <h2>{translate('quality_profiles.change_parent')}</h2> + </div> + <div className="modal-body"> + <div className="modal-field"> + <label htmlFor="change-profile-parent"> + {translate('quality_profiles.parent')}: <em className="mandatory">*</em> + </label> + <Select + clearable={false} + id="change-profile-parent" + name="parentKey" + onChange={this.handleSelectChange} + options={options} + value={ + this.state.selected != null + ? this.state.selected + : this.props.profile.parentKey || '' + } + /> + </div> + </div> + <div className="modal-foot"> + {this.state.loading && <i className="spinner spacer-right" />} + <button disabled={submitDisabled} id="change-profile-parent-submit"> + {translate('change_verb')} + </button> + <a href="#" id="change-profile-parent-cancel" onClick={this.handleCancelClick}> + {translate('cancel')} + </a> + </div> + </form> + + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.js index b9601e3eb5c..6f46189d3e3 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeProjectsView.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.js @@ -18,39 +18,47 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // @flow +import React from 'react'; +import Modal from 'react-modal'; import escapeHtml from 'escape-html'; -import ModalFormView from '../../../components/common/modal-form'; -import Template from '../templates/quality-profiles-change-projects.hbs'; +import type { Profile } from '../propTypes'; import { translate } from '../../../helpers/l10n'; -import '../../../components/SelectList'; -export default ModalFormView.extend({ - template: Template, +type Props = { + onClose: () => void, + organization: ?string, + profile: Profile +}; - onRender() { - // TODO remove uuid usage +export default class ChangeProjectsForm extends React.PureComponent { + container: HTMLElement; + props: Props; - ModalFormView.prototype.onRender.apply(this, arguments); + componentDidMount() { + this.renderSelectList(); + } + + handleCloseClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; - const { key } = this.options.profile; + renderSelectList() { + const { key } = this.props.profile; const searchUrl = window.baseUrl + '/api/qualityprofiles/projects?key=' + encodeURIComponent(key); new window.SelectList({ searchUrl, - el: this.$('#profile-projects'), + el: this.container, width: '100%', readOnly: false, focusSearch: false, - dangerouslyUnescapedHtmlFormat(item) { - return escapeHtml(item.name); - }, + dangerouslyUnescapedHtmlFormat: item => escapeHtml(item.name), selectUrl: window.baseUrl + '/api/qualityprofiles/add_project', deselectUrl: window.baseUrl + '/api/qualityprofiles/remove_project', - extra: { - profileKey: key - }, + extra: { profileKey: key }, selectParameter: 'projectUuid', selectParameterValue: 'uuid', labels: { @@ -64,10 +72,32 @@ export default ModalFormView.extend({ deselect: translate('quality_profiles.projects.deselect_hint') } }); - }, + } + + render() { + const header = translate('projects'); + + return ( + <Modal + isOpen={true} + contentLabel={header} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <div className="modal-head"> + <h2>{header}</h2> + </div> + + <div className="modal-body"> + <div id="profile-projects" ref={node => (this.container = node)} /> + </div> + + <div className="modal-foot"> + <a href="#" onClick={this.handleCloseClick}>{translate('close')}</a> + </div> - onDestroy() { - this.options.loadProjects(); - ModalFormView.prototype.onDestroy.apply(this, arguments); + </Modal> + ); } -}); +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js index 7b7a296536c..9417f4cf0c9 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileDetails.js @@ -28,6 +28,7 @@ import type { Profile, Exporter } from '../propTypes'; type Props = { canAdmin: boolean, exporters: Array<Exporter>, + onRequestFail: Object => void, organization: ?string, profile: Profile, profiles: Array<Profile>, diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js index 5c66dfcfcac..02e1b508ff2 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.js @@ -34,6 +34,7 @@ import type { Profile } from '../propTypes'; type Props = { canAdmin: boolean, + onRequestFail: Object => void, organization: ?string, profile: Profile, updateProfiles: () => Promise<*> @@ -136,6 +137,7 @@ export default class ProfileHeader extends React.PureComponent { </button> <ProfileActions canAdmin={this.props.canAdmin} + onRequestFail={this.props.onRequestFail} organization={organization} profile={profile} updateProfiles={this.props.updateProfiles} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js index cc8a8a9f535..ba11b5a9e5d 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritance.js @@ -21,13 +21,14 @@ import React from 'react'; import classNames from 'classnames'; import ProfileInheritanceBox from './ProfileInheritanceBox'; -import ChangeParentView from '../views/ChangeParentView'; +import ChangeParentForm from './ChangeParentForm'; import { translate } from '../../../helpers/l10n'; import { getProfileInheritance } from '../../../api/quality-profiles'; import type { Profile } from '../propTypes'; type Props = { canAdmin: boolean, + onRequestFail: Object => void, organization: ?string, profile: Profile, profiles: Array<Profile>, @@ -45,6 +46,7 @@ type ProfileInheritanceDetails = { type State = { ancestors?: Array<ProfileInheritanceDetails>, children?: Array<ProfileInheritanceDetails>, + formOpen: boolean, loading: boolean, profile?: ProfileInheritanceDetails }; @@ -53,6 +55,7 @@ export default class ProfileInheritance extends React.PureComponent { mounted: boolean; props: Props; state: State = { + formOpen: false, loading: true }; @@ -85,14 +88,23 @@ export default class ProfileInheritance extends React.PureComponent { }); } - handleChangeParent = (e: SyntheticInputEvent) => { - e.preventDefault(); - new ChangeParentView({ profile: this.props.profile, profiles: this.props.profiles }) - .on('done', () => this.props.updateProfiles()) - .render(); + handleChangeParentClick = (event: Event) => { + event.preventDefault(); + this.setState({ formOpen: true }); + }; + + closeForm = () => { + this.setState({ formOpen: false }); + }; + + handleParentChange = () => { + this.props.updateProfiles(); + this.closeForm(); }; render() { + const { profile, profiles } = this.props; + const highlightCurrent = !this.state.loading && this.state.ancestors != null && @@ -109,7 +121,7 @@ export default class ProfileInheritance extends React.PureComponent { {translate('quality_profiles.profile_inheritance')} </h2> {this.props.canAdmin && - <button className="pull-right js-change-parent" onClick={this.handleChangeParent}> + <button className="pull-right js-change-parent" onClick={this.handleChangeParentClick}> {translate('quality_profiles.change_parent')} </button>} </header> @@ -123,7 +135,7 @@ export default class ProfileInheritance extends React.PureComponent { className="js-inheritance-ancestor" depth={index} key={ancestor.key} - language={this.props.profile.language} + language={profile.language} organization={this.props.organization} profile={ancestor} /> @@ -133,7 +145,7 @@ export default class ProfileInheritance extends React.PureComponent { className={currentClassName} depth={this.state.ancestors ? this.state.ancestors.length : 0} displayLink={false} - language={this.props.profile.language} + language={profile.language} organization={this.props.organization} profile={this.state.profile} /> @@ -144,13 +156,22 @@ export default class ProfileInheritance extends React.PureComponent { className="js-inheritance-child" depth={this.state.ancestors ? this.state.ancestors.length + 1 : 0} key={child.key} - language={this.props.profile.language} + language={profile.language} organization={this.props.organization} profile={child} /> ))} </tbody> </table>} + + {this.state.formOpen && + <ChangeParentForm + onChange={this.handleParentChange} + onClose={this.closeForm} + onRequestFail={this.props.onRequestFail} + profile={profile} + profiles={profiles.filter(p => p !== profile && p.language === profile.language)} + />} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js index 171d4ab8e0d..e3b08402dd6 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.js @@ -20,7 +20,7 @@ // @flow import React from 'react'; import { Link } from 'react-router'; -import ChangeProjectsView from '../views/ChangeProjectsView'; +import ChangeProjectsForm from './ChangeProjectsForm'; import QualifierIcon from '../../../components/shared/QualifierIcon'; import { getProfileProjects } from '../../../api/quality-profiles'; import { translate } from '../../../helpers/l10n'; @@ -34,6 +34,7 @@ type Props = { }; type State = { + formOpen: boolean, loading: boolean, more?: boolean, projects: ?Array<*> @@ -43,6 +44,7 @@ export default class ProfileProjects extends React.PureComponent { mounted: boolean; props: Props; state: State = { + formOpen: false, loading: true, projects: null }; @@ -79,15 +81,15 @@ export default class ProfileProjects extends React.PureComponent { }); } - handleChange(e: SyntheticInputEvent) { - e.preventDefault(); - e.target.blur(); - new ChangeProjectsView({ - loadProjects: this.props.updateProfiles, - organization: this.props.organization, - profile: this.props.profile - }).render(); - } + handleChangeClick = (event: Event) => { + event.preventDefault(); + this.setState({ formOpen: true }); + }; + + closeForm = () => { + this.setState({ formOpen: false }); + this.props.updateProfiles(); + }; renderDefault() { return ( @@ -143,13 +145,20 @@ export default class ProfileProjects extends React.PureComponent { {this.props.canAdmin && !this.props.profile.isDefault && <div className="pull-right"> - <button className="js-change-projects" onClick={this.handleChange.bind(this)}> + <button className="js-change-projects" onClick={this.handleChangeClick}> {translate('quality_profiles.change_projects')} </button> </div>} </header> {this.props.profile.isDefault ? this.renderDefault() : this.renderProjects()} + + {this.state.formOpen && + <ChangeProjectsForm + onClose={this.closeForm} + organization={this.props.organization} + profile={this.props.profile} + />} </div> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.js new file mode 100644 index 00000000000..4219dbd37f6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.js @@ -0,0 +1,197 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import Select from 'react-select'; +import { sortBy } from 'lodash'; +import { getImporters, createQualityProfile } from '../../../api/quality-profiles'; +import { translate } from '../../../helpers/l10n'; + +type Props = { + languages: Array<{ key: string, name: string }>, + onClose: () => void, + onCreate: Function, + onRequestFail: Object => void, + organization: ?string +}; + +type State = { + importers: Array<{ key: string, languages: Array<string>, name: string }>, + language?: string, + loading: boolean, + name: string, + preloading: boolean +}; + +export default class CreateProfileForm extends React.PureComponent { + form: HTMLFormElement; + mounted: boolean; + props: Props; + state: State = { importers: [], loading: false, name: '', preloading: true }; + + componentDidMount() { + this.mounted = true; + this.fetchImporters(); + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchImporters() { + getImporters().then(importers => { + if (this.mounted) { + this.setState({ importers, preloading: false }); + } + }); + } + + handleCancelClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleNameChange = (event: { currentTarget: HTMLInputElement }) => { + this.setState({ name: event.currentTarget.value }); + }; + + handleLanguageChange = (option: { value: string }) => { + this.setState({ language: option.value }); + }; + + handleFormSubmit = (event: Event) => { + event.preventDefault(); + + this.setState({ loading: true }); + + const data = new FormData(this.form); + if (this.props.organization) { + data.append('organization', this.props.organization); + } + + createQualityProfile(data).then( + response => this.props.onCreate(response.profile), + error => { + if (this.mounted) { + this.setState({ loading: false }); + } + this.props.onRequestFail(error); + } + ); + }; + + render() { + const header = translate('quality_profiles.new_profile'); + + const languages = sortBy(this.props.languages, 'name'); + const selectedLanguage = this.state.language || languages[0].key; + const importers = this.state.importers.filter(importer => + importer.languages.includes(selectedLanguage) + ); + + return ( + <Modal + isOpen={true} + contentLabel={header} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <form + id="create-profile-form" + onSubmit={this.handleFormSubmit} + ref={node => (this.form = node)}> + + <div className="modal-head"> + <h2>{header}</h2> + </div> + + {this.state.preloading + ? <div className="modal-body"><i className="spinner" /></div> + : <div className="modal-body"> + <div className="modal-field"> + <label htmlFor="create-profile-name"> + {translate('name')}<em className="mandatory">*</em> + </label> + <input + autoFocus={true} + id="create-profile-name" + maxLength="100" + name="name" + onChange={this.handleNameChange} + required={true} + size="50" + type="text" + value={this.state.name} + /> + </div> + <div className="modal-field spacer-bottom"> + <label htmlFor="create-profile-language"> + {translate('language')}<em className="mandatory">*</em> + </label> + <Select + clearable={false} + id="create-profile-language" + name="language" + onChange={this.handleLanguageChange} + options={languages.map(language => ({ + label: language.name, + value: language.key + }))} + value={selectedLanguage} + /> + </div> + {importers.map(importer => ( + <div + className="modal-field spacer-bottom js-importer" + data-key={importer.key} + key={importer.key}> + <label htmlFor={'create-profile-form-backup-' + importer.key}> + {importer.name} + </label> + <input + id={'create-profile-form-backup-' + importer.key} + name={'backup_' + importer.key} + type="file" + /> + <p className="note"> + {translate('quality_profiles.optional_configuration_file')} + </p> + </div> + ))} + </div>} + + <div className="modal-foot"> + {this.state.loading && <i className="spinner spacer-right" />} + {!this.state.preloading && + <button disabled={this.state.loading} id="create-profile-submit"> + {translate('create')} + </button>} + <a href="#" id="create-profile-cancel" onClick={this.handleCancelClick}> + {translate('cancel')} + </a> + </div> + </form> + + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js index df44389a9c6..70389154ed4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/HomeContainer.js @@ -28,6 +28,7 @@ type Props = { canAdmin: boolean, languages: Array<{ key: string, name: string }>, location: { query: { [string]: string } }, + onRequestFail: Object => void, organization?: string, profiles: Array<Profile>, updateProfiles: () => Promise<*> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js index df6191a8300..6a276e243ee 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/PageHeader.js @@ -19,76 +19,63 @@ */ // @flow import React from 'react'; -import CreateProfileView from '../views/CreateProfileView'; -import RestoreProfileView from '../views/RestoreProfileView'; +import CreateProfileForm from './CreateProfileForm'; +import RestoreProfileForm from './RestoreProfileForm'; import RestoreBuiltInProfilesView from '../views/RestoreBuiltInProfilesView'; +import type { Profile } from '../propTypes'; import { getProfilePath } from '../utils'; import { translate } from '../../../helpers/l10n'; -import { getImporters } from '../../../api/quality-profiles'; type Props = { canAdmin: boolean, languages: Array<{ key: string, name: string }>, + onRequestFail: Object => void, organization: ?string, updateProfiles: () => Promise<*> }; +type State = { + createFormOpen: boolean, + restoreFormOpen: boolean +}; + export default class PageHeader extends React.PureComponent { - mounted: boolean; props: Props; static contextTypes = { router: React.PropTypes.object }; - state = {}; + state: State = { + createFormOpen: false, + restoreFormOpen: false + }; - componentDidMount() { - this.mounted = true; - } + handleCreateClick = (event: Event & { currentTarget: HTMLButtonElement }) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ createFormOpen: true }); + }; - componentWillUnmount() { - this.mounted = false; - } + handleCreate = (profile: Profile) => { + this.props.updateProfiles().then(() => { + this.context.router.push( + getProfilePath(profile.name, profile.language, this.props.organization) + ); + }); + }; - retrieveImporters() { - if (this.state.importers) { - return Promise.resolve(this.state.importers); - } else { - return getImporters().then(importers => { - this.setState({ importers }); - return importers; - }); - } - } + closeCreateForm = () => { + this.setState({ createFormOpen: false }); + }; - handleCreateClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - e.target.blur(); - this.retrieveImporters().then(importers => { - new CreateProfileView({ - languages: this.props.languages, - organization: this.props.organization, - importers - }) - .on('done', profile => { - this.props.updateProfiles().then(() => { - this.context.router.push( - getProfilePath(profile.name, profile.language, this.props.organization) - ); - }); - }) - .render(); - }); + handleRestoreClick = (event: Event) => { + event.preventDefault(); + this.setState({ restoreFormOpen: true }); }; - handleRestoreClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - new RestoreProfileView({ - organization: this.props.organization - }) - .on('done', this.props.updateProfiles) - .render(); + closeRestoreForm = () => { + this.setState({ restoreFormOpen: false }); }; handleRestoreBuiltIn = (e: SyntheticInputEvent) => { @@ -139,6 +126,23 @@ export default class PageHeader extends React.PureComponent { <br /> {translate('quality_profiles.intro2')} </div> + + {this.state.restoreFormOpen && + <RestoreProfileForm + onClose={this.closeRestoreForm} + onRequestFail={this.props.onRequestFail} + onRestore={this.props.updateProfiles} + organization={this.props.organization} + />} + + {this.state.createFormOpen && + <CreateProfileForm + languages={this.props.languages} + onClose={this.closeCreateForm} + onRequestFail={this.props.onRequestFail} + onCreate={this.handleCreate} + organization={this.props.organization} + />} </header> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js index c2b0681f686..dbcf7840677 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesList.js @@ -30,6 +30,7 @@ type Props = { canAdmin: boolean, languages: Array<{ key: string, name: string }>, location: { query: { [string]: string } }, + onRequestFail: Object => void, organization: ?string, profiles: Array<Profile>, updateProfiles: () => Promise<*> @@ -43,6 +44,7 @@ export default class ProfilesList extends React.PureComponent { <ProfilesListRow canAdmin={this.props.canAdmin} key={profile.key} + onRequestFail={this.props.onRequestFail} organization={this.props.organization} profile={profile} updateProfiles={this.props.updateProfiles} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js index 85dcb48c1d3..ec8bcf086e4 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/ProfilesListRow.js @@ -30,6 +30,7 @@ import type { Profile } from '../propTypes'; type Props = { canAdmin: boolean, + onRequestFail: Object => void, organization: ?string, profile: Profile, updateProfiles: () => Promise<*> @@ -160,6 +161,7 @@ export default class ProfilesListRow extends React.PureComponent { <ProfileActions canAdmin={this.props.canAdmin} fromList={true} + onRequestFail={this.props.onRequestFail} organization={this.props.organization} profile={this.props.profile} updateProfiles={this.props.updateProfiles} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/home/RestoreProfileForm.js b/server/sonar-web/src/main/js/apps/quality-profiles/home/RestoreProfileForm.js new file mode 100644 index 00000000000..911482ee914 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-profiles/home/RestoreProfileForm.js @@ -0,0 +1,159 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Modal from 'react-modal'; +import { restoreQualityProfile } from '../../../api/quality-profiles'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; + +type Props = { + onClose: () => void, + onRequestFail: Object => void, + onRestore: Function, + organization: ?string +}; + +type State = { + loading: boolean, + profile?: { name: string }, + ruleFailures?: number, + ruleSuccesses?: number +}; + +export default class RestoreProfileForm extends React.PureComponent { + form: HTMLFormElement; + mounted: boolean; + props: Props; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: Event) => { + event.preventDefault(); + this.props.onClose(); + }; + + handleFormSubmit = (event: Event) => { + event.preventDefault(); + + this.setState({ loading: true }); + + const data = new FormData(this.form); + if (this.props.organization) { + data.append('organization', this.props.organization); + } + + restoreQualityProfile(data).then( + response => { + if (this.mounted) { + this.setState({ + loading: false, + profile: response.profile, + ruleFailures: response.ruleFailures, + ruleSuccesses: response.ruleSuccesses + }); + } + this.props.onRestore(); + }, + error => { + if (this.mounted) { + this.setState({ loading: false }); + } + this.props.onRequestFail(error); + } + ); + }; + + render() { + const header = translate('quality_profiles.restore_profile'); + + const { loading, profile, ruleFailures, ruleSuccesses } = this.state; + + return ( + <Modal + isOpen={true} + contentLabel={header} + className="modal" + overlayClassName="modal-overlay" + onRequestClose={this.props.onClose}> + + <form + id="restore-profile-form" + onSubmit={this.handleFormSubmit} + ref={node => (this.form = node)}> + + <div className="modal-head"> + <h2>{header}</h2> + </div> + + <div className="modal-body"> + {profile != null && ruleSuccesses != null + ? ruleFailures + ? <div className="alert alert-warning"> + {translateWithParameters( + 'quality_profiles.restore_profile.warning', + profile.name, + ruleSuccesses, + ruleFailures + )} + </div> + : <div className="alert alert-success"> + {translateWithParameters( + 'quality_profiles.restore_profile.success', + profile.name, + ruleSuccesses + )} + </div> + : <div className="modal-field"> + <label htmlFor="restore-profile-backup"> + {translate('backup')}<em className="mandatory">*</em> + </label> + <input id="restore-profile-backup" name="backup" required={true} type="file" /> + </div>} + </div> + + {ruleSuccesses == null + ? <div className="modal-foot"> + {loading && <i className="spinner spacer-right" />} + <button disabled={loading} id="restore-profile-submit"> + {translate('restore')} + </button> + <a href="#" id="restore-profile-cancel" onClick={this.handleCancelClick}> + {translate('cancel')} + </a> + </div> + : <div className="modal-foot"> + <a href="#" onClick={this.handleCancelClick}> + {translate('close')} + </a> + </div>} + + </form> + + </Modal> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js b/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js index f8438011916..92af7067b7b 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js +++ b/server/sonar-web/src/main/js/apps/quality-profiles/propTypes.js @@ -37,7 +37,8 @@ export type Profile = { userUpdatedAt?: string, lastUsed?: string, rulesUpdatedAt: string, - depth: number + depth: number, + childrenCount: number }; export type Exporter = { diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-profile-parent.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-profile-parent.hbs deleted file mode 100644 index 94c7e29c60a..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-profile-parent.hbs +++ /dev/null @@ -1,21 +0,0 @@ -<form id="change-profile-parent-form"> - <div class="modal-head"> - <h2>{{t 'quality_profiles.change_parent'}}</h2> - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - <div class="modal-field"> - <label for="change-profile-parent">Parent: <em class="mandatory">*</em></label> - <select id="change-profile-parent" name="parentKey"> - <option value="#none">{{t 'none'}}</option> - {{#each profiles}} - <option value="{{key}}">{{name}}</option> - {{/each}} - </select> - </div> - </div> - <div class="modal-foot"> - <button id="change-profile-parent-submit">{{t 'change_verb'}}</button> - <a href="#" class="js-modal-close" id="change-profile-parent-cancel">{{t 'cancel'}}</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs deleted file mode 100644 index 09e591327e2..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-change-projects.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div class="modal-head"> - <h2>{{t 'projects'}}</h2> -</div> - -<div class="modal-body"> - <div class="js-modal-messages"></div> - <div id="profile-projects"></div> -</div> - -<div class="modal-foot"> - <a href="#" class="js-modal-close">{{t 'close'}}</a> -</div> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-copy-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-copy-profile.hbs deleted file mode 100644 index 6cc034f1209..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-copy-profile.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<form id="copy-profile-form"> - <div class="modal-head"> - <h2>{{tp 'quality_profiles.copy_x_title' name languageName}}</h2> - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - <div class="modal-field"> - <label for="copy-profile-name">{{t 'quality_profiles.copy_new_name'}}<em class="mandatory">*</em></label> - <input id="copy-profile-name" name="name" type="text" size="50" maxlength="100" required> - </div> - </div> - <div class="modal-foot"> - <button id="copy-profile-submit">{{t 'copy'}}</button> - <a href="#" class="js-modal-close" id="copy-profile-cancel">{{t 'cancel'}}</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs deleted file mode 100644 index acdc7f93805..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-create-profile.hbs +++ /dev/null @@ -1,34 +0,0 @@ -<form id="create-profile-form" action="{{link '/api/qualityprofiles/create'}}" enctype="multipart/form-data" method="POST"> - <div class="modal-head"> - <h2>{{t 'quality_profiles.new_profile'}}</h2> - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - {{#if organization}} - <input type="hidden" name="organization" value="{{organization}}"> - {{/if}} - <div class="modal-field"> - <label for="create-profile-name">{{t 'name'}}<em class="mandatory">*</em></label> - <input id="create-profile-name" name="name" type="text" size="50" maxlength="100" required> - </div> - <div class="modal-field spacer-bottom"> - <label for="create-profile-language">{{t 'language'}}<em class="mandatory">*</em></label> - <select id="create-profile-language" name="language" required> - {{#each languages}} - <option value="{{key}}">{{name}}</option> - {{/each}} - </select> - </div> - {{#each importers}} - <div class="modal-field spacer-bottom js-importer" data-key="{{key}}"> - <label for="create-profile-form-backup-{{key}}">{{name}}</label> - <input id="create-profile-form-backup-{{key}}" name="backup_{{key}}" type="file"> - <p class="note">{{t 'quality_profiles.optional_configuration_file'}}</p> - </div> - {{/each}} - </div> - <div class="modal-foot"> - <button id="create-profile-submit">{{t 'create'}}</button> - <a href="#" class="js-modal-close" id="create-profile-cancel">{{t 'cancel'}}</a> - </div> -</form>
\ No newline at end of file diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs deleted file mode 100644 index 17438526340..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-delete-profile.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<form id="delete-profile-form"> - <div class="modal-head"> - <h2>{{t 'quality_profiles.delete_confirm_title'}}</h2> - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - {{#if childrenCount}} - <div class="alert alert-warning">{{t 'quality_profiles.this_profile_has_descendants'}}</div> - <p>{{tp 'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants' name languageName}}</p> - {{else}} - <p>{{tp 'quality_profiles.are_you_sure_want_delete_profile_x' name languageName}}</p> - {{/if}} - </div> - <div class="modal-foot"> - <button id="delete-profile-submit">{{t 'delete'}}</button> - <a href="#" class="js-modal-close" id="delete-profile-cancel">{{t 'cancel'}}</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-rename-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-rename-profile.hbs deleted file mode 100644 index 3da132f8f4f..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-rename-profile.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<form id="rename-profile-form"> - <div class="modal-head"> - <h2>{{tp 'quality_profiles.rename_x_title' name languageName}}</h2> - </div> - <div class="modal-body"> - <div class="js-modal-messages"></div> - <div class="modal-field"> - <label for="rename-profile-name">{{t 'quality_profiles.new_name'}} <em class="mandatory">*</em></label> - <input id="rename-profile-name" name="name" type="text" size="50" maxlength="100" value="{{name}}" required> - </div> - </div> - <div class="modal-foot"> - <button id="rename-profile-submit">{{t 'rename'}}</button> - <a href="#" class="js-modal-close" id="rename-profile-cancel">{{t 'cancel'}}</a> - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-profile.hbs b/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-profile.hbs deleted file mode 100644 index 7c22c83aab5..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/templates/quality-profiles-restore-profile.hbs +++ /dev/null @@ -1,37 +0,0 @@ -<form id="restore-profile-form"> - <div class="modal-head"> - <h2>{{t 'quality_profiles.restore_profile'}}</h2> - </div> - - <div class="modal-body"> - <div class="js-modal-messages"></div> - {{#if organization}} - <input type="hidden" name="organization" value="{{organization}}"> - {{/if}} - {{#if profile}} - {{#if ruleFailures}} - <div class="alert alert-warning"> - {{tp 'quality_profiles.restore_profile.warning' profile.name ruleSuccesses ruleFailures}} - </div> - {{else}} - <div class="alert alert-success"> - {{tp 'quality_profiles.restore_profile.success' profile.name ruleSuccesses}} - </div> - {{/if}} - {{else}} - <div class="modal-field"> - <label for="restore-profile-backup">{{t 'backup'}}<em class="mandatory">*</em></label> - <input type="file" id="restore-profile-backup" name="backup" required> - </div> - {{/if}} - </div> - - <div class="modal-foot"> - {{#notNull ruleSuccesses}} - <a href="#" class="js-modal-close">{{t 'close'}}</a> - {{else}} - <button id="restore-profile-submit">{{t 'restore'}}</button> - <a href="#" class="js-modal-close" id="restore-profile-cancel">{{t 'cancel'}}</a> - {{/notNull}} - </div> -</form> diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js deleted file mode 100644 index 912aecb10cb..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/views/ChangeParentView.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import ModalFormView from '../../../components/common/modal-form'; -import Template from '../templates/quality-profiles-change-profile-parent.hbs'; -import { changeProfileParent } from '../../../api/quality-profiles'; - -export default ModalFormView.extend({ - template: Template, - - onRender() { - ModalFormView.prototype.onRender.apply(this, arguments); - this.$('select').select2({ - allowClear: false, - width: '250px', - minimumResultsForSearch: 50 - }); - }, - - onFormSubmit() { - ModalFormView.prototype.onFormSubmit.apply(this, arguments); - this.disableForm(); - this.sendRequest(); - }, - - sendRequest() { - let parent = this.$('#change-profile-parent').val(); - if (parent === '#none') { - parent = ''; - } - changeProfileParent(this.options.profile.key, parent) - .then(() => { - this.destroy(); - this.trigger('done'); - }) - .catch(e => { - if (e.response.status === 400) { - this.enableForm(); - e.response.json().then(r => this.showErrors(r.errors, r.warnings)); - } - }); - }, - - serializeData() { - const { profile } = this.options; - const profiles = this.options.profiles.filter( - p => p !== profile && p.language === profile.language - ); - return { ...profile, profiles }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js deleted file mode 100644 index 711946b8bfa..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/views/CopyProfileView.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import ModalFormView from '../../../components/common/modal-form'; -import Template from '../templates/quality-profiles-copy-profile.hbs'; -import { copyProfile } from '../../../api/quality-profiles'; - -export default ModalFormView.extend({ - template: Template, - - onFormSubmit() { - ModalFormView.prototype.onFormSubmit.apply(this, arguments); - this.disableForm(); - this.sendRequest(); - }, - - sendRequest() { - const name = this.$('#copy-profile-name').val(); - copyProfile(this.options.profile.key, name) - .then(profile => { - this.destroy(); - this.trigger('done', profile); - }) - .catch(e => { - if (e.response.status === 400) { - this.enableForm(); - e.response.json().then(r => this.showErrors(r.errors, r.warnings)); - } - }); - }, - - serializeData() { - return this.options.profile; - } -}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js deleted file mode 100644 index fb74811ec69..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/views/CreateProfileView.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import $ from 'jquery'; -import ModalFormView from '../../../components/common/modal-form'; -import Template from '../templates/quality-profiles-create-profile.hbs'; -import { createQualityProfile } from '../../../api/quality-profiles'; - -export default ModalFormView.extend({ - template: Template, - - events() { - return { - ...ModalFormView.prototype.events.apply(this, arguments), - 'change #create-profile-language': 'onLanguageChange' - }; - }, - - onFormSubmit() { - ModalFormView.prototype.onFormSubmit.apply(this, arguments); - - const form = this.$('form')[0]; - const data = new FormData(form); - - createQualityProfile(data) - .then(r => { - this.trigger('done', r.profile); - this.destroy(); - }) - .catch(e => { - e.response.json().then(r => this.showErrors(r.errors, r.warnings)); - }); - }, - - onRender() { - ModalFormView.prototype.onRender.apply(this, arguments); - this.$('select').select2({ - width: '250px', - minimumResultsForSearch: 50 - }); - this.onLanguageChange(); - }, - - onLanguageChange() { - const that = this; - const language = this.$('#create-profile-language').val(); - const importers = this.getImportersForLanguages(language); - this.$('.js-importer').each(function() { - that.emptyInput($(this)); - $(this).addClass('hidden'); - }); - importers.forEach(importer => { - that.$(`.js-importer[data-key="${importer.key}"]`).removeClass('hidden'); - }); - }, - - emptyInput(e) { - e.wrap('<form>').closest('form').get(0).reset(); - e.unwrap(); - }, - - getImportersForLanguages(language) { - if (language != null) { - return this.options.importers.filter(importer => importer.languages.indexOf(language) !== -1); - } else { - return []; - } - }, - - serializeData() { - return { - ...ModalFormView.prototype.serializeData.apply(this, arguments), - languages: this.options.languages, - importers: this.options.importers, - organization: this.options.organization - }; - } -}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js deleted file mode 100644 index ec4c5f878a4..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/views/DeleteProfileView.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import ModalFormView from '../../../components/common/modal-form'; -import Template from '../templates/quality-profiles-delete-profile.hbs'; -import { deleteProfile } from '../../../api/quality-profiles'; - -export default ModalFormView.extend({ - template: Template, - - modelEvents: { - destroy: 'destroy' - }, - - onFormSubmit() { - ModalFormView.prototype.onFormSubmit.apply(this, arguments); - this.disableForm(); - this.sendRequest(); - }, - - sendRequest() { - deleteProfile(this.options.profile.key) - .then(() => { - this.destroy(); - this.trigger('done'); - }) - .catch(e => { - if (e.response.status === 400) { - this.enableForm(); - e.response.json().then(r => this.showErrors(r.errors, r.warnings)); - } - }); - }, - - serializeData() { - return this.options.profile; - } -}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js deleted file mode 100644 index 57e3e5c5e67..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/views/RenameProfileView.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import ModalFormView from '../../../components/common/modal-form'; -import Template from '../templates/quality-profiles-rename-profile.hbs'; -import { renameProfile } from '../../../api/quality-profiles'; - -export default ModalFormView.extend({ - template: Template, - - onFormSubmit() { - ModalFormView.prototype.onFormSubmit.apply(this, arguments); - this.sendRequest(); - }, - - sendRequest() { - const name = this.$('#rename-profile-name').val(); - renameProfile(this.options.profile.key, name) - .then(() => { - this.destroy(); - this.trigger('done', name); - }) - .catch(e => { - if (e.response.status === 400) { - this.enableForm(); - e.response.json().then(r => this.showErrors(r.errors, r.warnings)); - } - }); - }, - - serializeData() { - return this.options.profile; - } -}); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js b/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js deleted file mode 100644 index d2393e5053d..00000000000 --- a/server/sonar-web/src/main/js/apps/quality-profiles/views/RestoreProfileView.js +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -// @flow -import ModalFormView from '../../../components/common/modal-form'; -import Template from '../templates/quality-profiles-restore-profile.hbs'; -import { restoreQualityProfile } from '../../../api/quality-profiles'; - -export default ModalFormView.extend({ - template: Template, - - onFormSubmit(e) { - ModalFormView.prototype.onFormSubmit.apply(this, arguments); - const data = new FormData(e.currentTarget); - - this.disableForm(); - - restoreQualityProfile(data) - .then(r => { - this.profile = r.profile; - this.ruleSuccesses = r.ruleSuccesses; - this.ruleFailures = r.ruleFailures; - this.render(); - this.trigger('done'); - }) - .catch(e => { - this.enableForm(); - e.response.json().then(r => this.showErrors(r.errors, r.warnings)); - }); - }, - - serializeData() { - return { - ...ModalFormView.prototype.serializeData.apply(this, arguments), - organization: this.options.organization, - profile: this.profile, - ruleSuccesses: this.ruleSuccesses, - ruleFailures: this.ruleFailures - }; - } -}); |