aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/components.js26
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/app.js4
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js127
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js75
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js111
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js53
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/Header.js36
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/Key.js134
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js76
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js92
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs30
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js48
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/actions.js28
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/components.js47
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js50
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js19
-rw-r--r--server/sonar-web/src/main/less/style.less1
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb62
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb18
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb19
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb74
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb61
22 files changed, 960 insertions, 231 deletions
diff --git a/server/sonar-web/src/main/js/api/components.js b/server/sonar-web/src/main/js/api/components.js
index f0f49a636b4..6b3238022fb 100644
--- a/server/sonar-web/src/main/js/api/components.js
+++ b/server/sonar-web/src/main/js/api/components.js
@@ -109,3 +109,29 @@ export function getMyProjects (data) {
const url = '/api/projects/search_my_projects';
return getJSON(url, data);
}
+
+/**
+ * Change component's key
+ * @param {string} key
+ * @param {string} newKey
+ * @returns {Promise}
+ */
+export function changeKey (key, newKey) {
+ const url = '/api/components/update_key';
+ const data = { key, newKey };
+ return post(url, data);
+}
+
+/**
+ * Bulk change component's key
+ * @param {string} key
+ * @param {string} from
+ * @param {string} to
+ * @param {boolean} dryRun
+ * @returns {Promise}
+ */
+export function bulkChangeKey (key, from, to, dryRun = false) {
+ const url = '/api/components/bulk_update_key';
+ const data = { key, from, to, dryRun };
+ return postJSON(url, data);
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/app.js b/server/sonar-web/src/main/js/apps/project-admin/app.js
index 7981c936e87..903ed046cb9 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/app.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/app.js
@@ -26,6 +26,7 @@ import Deletion from './deletion/Deletion';
import QualityProfiles from './quality-profiles/QualityProfiles';
import QualityGate from './quality-gate/QualityGate';
import Links from './links/Links';
+import Key from './key/Key';
import rootReducer from './store/rootReducer';
import configureStore from '../../components/store/configureStore';
@@ -56,6 +57,9 @@ window.sonarqube.appStarted.then(options => {
<Route
path="/links"
component={withComponent(Links)}/>
+ <Route
+ path="/key"
+ component={withComponent(Key)}/>
</Router>
</Provider>
), el);
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js
new file mode 100644
index 00000000000..1624faddc3f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js
@@ -0,0 +1,127 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import React from 'react';
+import BulkUpdateForm from './BulkUpdateForm';
+import BulkUpdateResults from './BulkUpdateResults';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { bulkChangeKey } from '../../../api/components';
+import { getComponentUrl } from '../../../helpers/urls';
+
+export default class BulkUpdate extends React.Component {
+ static propTypes = {
+ component: React.PropTypes.object.isRequired
+ };
+
+ state = {
+ updating: false,
+ updated: false,
+ newComponentKey: null
+ };
+
+ handleSubmit (replace, by) {
+ this.loadResults(replace, by);
+ }
+
+ handleConfirm () {
+ this.setState({ updating: true });
+
+ const { component } = this.props;
+ const { replace, by } = this.state;
+ bulkChangeKey(component.key, replace, by).then(r => {
+ const result = r.keys.find(result => result.key === component.key);
+ const newComponentKey = result != null ? result.newKey : component.key;
+ this.setState({
+ updating: false,
+ updated: true,
+ newComponentKey
+ });
+ });
+ }
+
+ loadResults (replace, by) {
+ const { component } = this.props;
+ bulkChangeKey(component.key, replace, by, true).then(r => {
+ this.setState({ results: r.keys, replace, by });
+ });
+ }
+
+ renderUpdating () {
+ return (
+ <div id="project-key-bulk-update">
+ <i className="spinner"/>
+ </div>
+ );
+ }
+
+ renderUpdated () {
+ return (
+ <div id="project-key-bulk-update">
+ <div className="alert alert-success">
+ {translate('update_key.key_updated')}
+ {' '}
+ <a href={getComponentUrl(this.state.newComponentKey)}>
+ {translate('check_project')}
+ </a>
+ </div>
+ </div>
+ );
+ }
+
+ render () {
+ const { component } = this.props;
+ const { updating, updated } = this.state;
+ const { results, replace, by } = this.state;
+
+ if (updating) {
+ return this.renderUpdating();
+ }
+
+ if (updated) {
+ return this.renderUpdated();
+ }
+
+ return (
+ <div id="project-key-bulk-update">
+ <header className="big-spacer-bottom">
+ <div className="spacer-bottom">
+ {translate('update_key.bulk_change_description')}
+ </div>
+ <div>
+ {translateWithParameters(
+ 'update_key.current_key_for_project_x_is_x',
+ component.name,
+ component.key
+ )}
+ </div>
+ </header>
+
+ <BulkUpdateForm onSubmit={this.handleSubmit.bind(this)}/>
+
+ {results != null && (
+ <BulkUpdateResults
+ results={results}
+ replace={replace}
+ by={by}
+ onConfirm={this.handleConfirm.bind(this)}/>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js
new file mode 100644
index 00000000000..db31d98063d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default class BulkUpdateForm extends React.Component {
+ static propTypes = {
+ onSubmit: React.PropTypes.func.isRequired
+ };
+
+ handleSubmit (e) {
+ e.preventDefault();
+ this.refs.submit.blur();
+
+ const replace = this.refs.replace.value;
+ const by = this.refs.by.value;
+
+ this.props.onSubmit(replace, by);
+ }
+
+ render () {
+ return (
+ <form onSubmit={this.handleSubmit.bind(this)}>
+ <div className="modal-field">
+ <label htmlFor="bulk-update-replace">
+ {translate('update_key.replace')}
+ </label>
+ <input
+ ref="replace"
+ id="bulk-update-replace"
+ name="replace"
+ type="text"
+ placeholder={translate('update_key.replace_example')}
+ required/>
+ </div>
+
+ <div className="modal-field">
+ <label htmlFor="bulk-update-by">
+ {translate('update_key.by')}
+ </label>
+ <input
+ ref="by"
+ id="bulk-update-by"
+ name="by"
+ type="text"
+ placeholder={translate('update_key.by_example')}
+ required/>
+ <button
+ ref="submit"
+ id="bulk-update-see-results"
+ className="big-spacer-left">
+ {translate('update_key.see_results')}
+ </button>
+ </div>
+ </form>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js
new file mode 100644
index 00000000000..56f5531875b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import React from 'react';
+import some from 'lodash/some';
+import { translateWithParameters, translate } from '../../../helpers/l10n';
+
+export default class BulkUpdateResults extends React.Component {
+ static propTypes = {
+ results: React.PropTypes.array.isRequired,
+ onConfirm: React.PropTypes.func.isRequired
+ };
+
+ handleConfirm (e) {
+ e.preventDefault();
+ e.target.blur();
+ this.props.onConfirm();
+ }
+
+ render () {
+ const { results, replace, by } = this.props;
+ const isEmpty = results.length === 0;
+ const hasDuplications = some(results, r => r.duplicate);
+ const canUpdate = !isEmpty && !hasDuplications;
+
+ return (
+ <div id="bulk-update-simulation" className="big-spacer-top">
+ {isEmpty && (
+ <div id="bulk-update-nothing" className="alert alert-warning">
+ {translateWithParameters(
+ 'update_key.no_key_to_update',
+ replace
+ )}
+ </div>
+ )}
+
+ {hasDuplications && (
+ <div id="bulk-update-duplicate" className="alert alert-danger">
+ {translateWithParameters(
+ 'update_key.cant_update_because_duplicate_keys',
+ replace,
+ by
+ )}
+ </div>
+ )}
+
+ {canUpdate && (
+ <div className="spacer-bottom">
+ {translate('update_key.keys_will_be_updated_as_follows')}
+ </div>
+ )}
+
+ {!isEmpty && (
+ <table
+ id="bulk-update-results"
+ className="data zebra zebra-hover">
+ <thead>
+ <tr>
+ <th>{translate('update_key.old_key')}</th>
+ <th>{translate('update_key.new_key')}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {results.map(result => (
+ <tr key={result.key} data-key={result.key}>
+ <td className="js-old-key">
+ {result.key}
+ </td>
+ <td className="js-new-key">
+ {result.duplicate && (
+ <span className="spacer-right badge badge-danger">
+ {translate('update_key.duplicate_key')}
+ </span>
+ )}
+ {result.newKey}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ )}
+
+ <div className="big-spacer-top">
+ {canUpdate && (
+ <button
+ id="bulk-update-confirm"
+ onClick={this.handleConfirm.bind(this)}>
+ {translate('update_verb')}
+ </button>
+ )}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
new file mode 100644
index 00000000000..6f7deacf0ed
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import React from 'react';
+import UpdateKeyForm from './UpdateKeyForm';
+import QualifierIcon from '../../../components/shared/qualifier-icon';
+import { translate } from '../../../helpers/l10n';
+
+export default class FineGrainedUpdate extends React.Component {
+ render () {
+ const { component, modules } = this.props;
+ const components = [component, ...modules];
+
+ return (
+ <div id="project-key-fine-grained-update">
+ <table className="data zebra">
+ <tbody>
+ {components.map(component => (
+ <tr key={component.key}>
+ <td className="width-40">
+ <QualifierIcon qualifier={component.qualifier}/>
+ {' '}
+ {component.name}
+ </td>
+ <td>
+ <UpdateKeyForm
+ component={component}
+ onKeyChange={this.props.onKeyChange}/>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Header.js b/server/sonar-web/src/main/js/apps/project-admin/key/Header.js
new file mode 100644
index 00000000000..c730528a8a2
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/Header.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import React from 'react';
+import { translate } from '../../../helpers/l10n';
+
+export default class Header extends React.Component {
+ render () {
+ return (
+ <header className="page-header">
+ <h1 className="page-title">
+ {translate('update_key.page')}
+ </h1>
+ <div className="page-description">
+ {translate('update_key.page.description')}
+ </div>
+ </header>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js
new file mode 100644
index 00000000000..47b61bc3e6e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js
@@ -0,0 +1,134 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import React from 'react';
+import shallowCompare from 'react-addons-shallow-compare';
+import { connect } from 'react-redux';
+import Header from './Header';
+import UpdateForm from './UpdateForm';
+import BulkUpdate from './BulkUpdate';
+import FineGrainedUpdate from './FineGrainedUpdate';
+import { getProjectModules } from '../store/rootReducer';
+import { fetchProjectModules, changeKey } from '../store/actions';
+import { translate } from '../../../helpers/l10n';
+
+class Key extends React.Component {
+ static propTypes = {
+ component: React.PropTypes.object.isRequired,
+ fetchProjectModules: React.PropTypes.func.isRequired,
+ changeKey: React.PropTypes.func.isRequired
+ };
+
+ state = {
+ tab: 'bulk'
+ };
+
+ componentDidMount () {
+ this.props.fetchProjectModules(this.props.component.key);
+ }
+
+ shouldComponentUpdate (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+
+ handleChangeKey (key, newKey) {
+ return this.props.changeKey(key, newKey).then(() => {
+ if (key === this.props.component.key) {
+ window.location = window.baseUrl +
+ '/project/key?id=' + encodeURIComponent(newKey);
+ }
+ });
+ }
+
+ handleChangeTab (tab, e) {
+ e.preventDefault();
+ e.target.blur();
+ this.setState({ tab });
+ }
+
+ render () {
+ const { component, modules } = this.props;
+
+ const noModules = modules != null && modules.length === 0;
+ const hasModules = modules != null && modules.length > 0;
+
+ const { tab } = this.state;
+
+ return (
+ <div id="project-key" className="page page-limited">
+ <Header/>
+
+ {modules == null && (
+ <i className="spinner"/>
+ )}
+
+ {noModules && (
+ <UpdateForm
+ component={component}
+ onKeyChange={this.handleChangeKey.bind(this)}/>
+ )}
+
+ {hasModules && (
+ <div>
+ <div className="big-spacer-bottom">
+ <ul className="tabs">
+ <li>
+ <a id="update-key-tab-bulk"
+ className={tab === 'bulk' ? 'selected' : ''}
+ href="#"
+ onClick={this.handleChangeTab.bind(this, 'bulk')}>
+ {translate('update_key.bulk_update')}
+ </a>
+ </li>
+ <li>
+ <a id="update-key-tab-fine"
+ className={tab === 'fine' ? 'selected' : ''}
+ href="#"
+ onClick={this.handleChangeTab.bind(this, 'fine')}>
+ {translate('update_key.fine_grained_key_update')}
+ </a>
+ </li>
+ </ul>
+ </div>
+
+ {tab === 'bulk' && (
+ <BulkUpdate component={component}/>
+ )}
+
+ {tab === 'fine' && (
+ <FineGrainedUpdate
+ component={component}
+ modules={modules}
+ onKeyChange={this.handleChangeKey.bind(this)}/>
+ )}
+ </div>
+ )}
+ </div>
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => ({
+ modules: getProjectModules(state, ownProps.component.key)
+});
+
+export default connect(
+ mapStateToProps,
+ { fetchProjectModules, changeKey }
+)(Key);
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js
new file mode 100644
index 00000000000..701d2070957
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import React from 'react';
+import UpdateKeyConfirmation from './views/UpdateKeyConfirmation';
+import { translate } from '../../../helpers/l10n';
+
+export default class UpdateForm extends React.Component {
+ static propTypes = {
+ component: React.PropTypes.object.isRequired,
+ onKeyChange: React.PropTypes.func.isRequired
+ };
+
+ state = { newKey: null };
+
+ handleSubmit (e) {
+ e.preventDefault();
+
+ const newKey = this.refs.newKey.value;
+
+ new UpdateKeyConfirmation({
+ newKey,
+ component: this.props.component,
+ onChange: this.props.onKeyChange
+ }).render();
+ }
+
+ handleChange (e) {
+ const newKey = e.target.value;
+ this.setState({ newKey });
+ }
+
+ render () {
+ const value = this.state.newKey != null ?
+ this.state.newKey :
+ this.props.component.key;
+
+ const hasChanged = value !== this.props.component.key;
+
+ return (
+ <form onSubmit={this.handleSubmit.bind(this)}>
+ <input
+ ref="newKey"
+ id="update-key-new-key"
+ className="input-super-large"
+ value={value}
+ type="text"
+ placeholder={translate('update_key.new_key')}
+ required
+ onChange={this.handleChange.bind(this)}/>
+
+ <div className="spacer-top">
+ <button id="update-key-submit" disabled={!hasChanged}>
+ {translate('update_verb')}
+ </button>
+ </div>
+ </form>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js
new file mode 100644
index 00000000000..f23d1f7069e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import React from 'react';
+import UpdateKeyConfirmation from './views/UpdateKeyConfirmation';
+import { translate } from '../../../helpers/l10n';
+
+export default class UpdateKeyForm extends React.Component {
+ static propTypes = {
+ component: React.PropTypes.object.isRequired
+ };
+
+ state = {};
+
+ componentWillMount () {
+ this.handleInputChange = this.handleInputChange.bind(this);
+ this.handleUpdateClick = this.handleUpdateClick.bind(this);
+ this.handleResetClick = this.handleResetClick.bind(this);
+ }
+
+ handleInputChange (e) {
+ const key = e.target.value;
+ this.setState({ key });
+ }
+
+ handleUpdateClick (e) {
+ e.preventDefault();
+ e.target.blur();
+
+ const newKey = this.refs.newKey.value;
+
+ new UpdateKeyConfirmation({
+ newKey,
+ component: this.props.component,
+ onChange: this.props.onKeyChange
+ }).render();
+ }
+
+ handleResetClick (e) {
+ e.preventDefault();
+ e.target.blur();
+ this.setState({ key: null });
+ }
+
+ render () {
+ const { component } = this.props;
+
+ const value = this.state.key != null ?
+ this.state.key :
+ component.key;
+
+ const hasChanged = this.state.key != null &&
+ this.state.key !== component.key;
+
+ return (
+ <div className="js-fine-grained-update" data-key={component.key}>
+ <input
+ ref="newKey"
+ className="input-super-large big-spacer-right"
+ type="text"
+ value={value}
+ onChange={this.handleInputChange}/>
+
+ <div className="button-group">
+ <button disabled={!hasChanged} onClick={this.handleUpdateClick}>
+ {translate('update_verb')}
+ </button>
+
+ <button disabled={!hasChanged} onClick={this.handleResetClick}>
+ {translate('reset_verb')}
+ </button>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs
new file mode 100644
index 00000000000..bec83b7693e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs
@@ -0,0 +1,30 @@
+<form id="update-key-confirmation-form" autocomplete="off">
+ <div class="modal-head">
+ <h2>{{t 'update_key.page'}}</h2>
+ </div>
+ <div class="modal-body">
+ <div class="js-modal-messages"></div>
+ {{tp 'update_key.are_you_sure_to_change_key' component.name}}
+ <div class="spacer-top">
+ <div class="display-inline-block text-right" style="width: 80px;">
+ {{t 'update_key.old_key'}}:
+ </div>
+ <div class="display-inline-block">
+ {{component.key}}
+ </div>
+ </div>
+ <div class="spacer-top">
+ <div class="display-inline-block text-right" style="width: 80px;">
+ {{t 'update_key.new_key'}}:
+ </div>
+ <div class="display-inline-block">
+ {{newKey}}
+ </div>
+ </div>
+ </div>
+ <div class="modal-foot">
+ <i class="js-modal-spinner spinner spacer-right hidden"></i>
+ <button id="update-key-confirm">{{t 'update_verb'}}</button>
+ <a href="#" class="js-modal-close">{{t 'cancel'}}</a>
+ </div>
+</form>
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js
new file mode 100644
index 00000000000..9b1c4abd26e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import ModalForm from '../../../../components/common/modal-form';
+import Template from './UpdateKeyConfirmation.hbs';
+import { parseError } from '../../../code/utils';
+
+export default ModalForm.extend({
+ template: Template,
+
+ onFormSubmit () {
+ ModalForm.prototype.onFormSubmit.apply(this, arguments);
+ this.disableForm();
+ this.showSpinner();
+
+ this.options.onChange(this.options.component.key, this.options.newKey)
+ .then(() => this.destroy())
+ .catch(e => {
+ parseError(e).then(msg => this.showSingleError(msg));
+ this.hideSpinner();
+ this.enableForm();
+ });
+ },
+
+ serializeData () {
+ return {
+ component: this.options.component,
+ newKey: this.options.newKey
+ };
+ }
+});
+
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
index 29a627e9d54..ca20001b763 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js
@@ -30,6 +30,8 @@ import {
dissociateGateWithProject
} from '../../../api/quality-gates';
import { getProjectLinks, createLink } from '../../../api/projectLinks';
+import { getTree } from '../../../api/components';
+import { changeKey as changeKeyApi } from '../../../api/components';
import { addGlobalSuccessMessage } from '../../../components/store/globalMessages';
import { translate, translateWithParameters } from '../../../helpers/l10n';
@@ -157,3 +159,29 @@ export const deleteProjectLink = (projectKey, linkId) => ({
projectKey,
linkId
});
+
+export const RECEIVE_PROJECT_MODULES = 'RECEIVE_PROJECT_MODULES';
+const receiveProjectModules = (projectKey, modules) => ({
+ type: RECEIVE_PROJECT_MODULES,
+ projectKey,
+ modules
+});
+
+export const fetchProjectModules = projectKey => dispatch => {
+ const options = { qualifiers: 'BRC', s: 'name', ps: 500 };
+ getTree(projectKey, options).then(r => {
+ dispatch(receiveProjectModules(projectKey, r.components));
+ });
+};
+
+export const CHANGE_KEY = 'CHANGE_KEY';
+const changeKeyAction = (key, newKey) => ({
+ type: CHANGE_KEY,
+ key,
+ newKey
+});
+
+export const changeKey = (key, newKey) => dispatch => {
+ return changeKeyApi(key, newKey)
+ .then(() => dispatch(changeKeyAction(key, newKey)));
+};
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/components.js b/server/sonar-web/src/main/js/apps/project-admin/store/components.js
new file mode 100644
index 00000000000..bc369003eee
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/components.js
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import keyBy from 'lodash/keyBy';
+import omit from 'lodash/omit';
+import { RECEIVE_PROJECT_MODULES, CHANGE_KEY } from './actions';
+
+const components = (state = {}, action = {}) => {
+ if (action.type === RECEIVE_PROJECT_MODULES) {
+ const newComponentsByKey = keyBy(action.modules, 'key');
+ return { ...state, ...newComponentsByKey };
+ }
+
+ if (action.type === CHANGE_KEY) {
+ const oldComponent = state[action.key];
+ if (oldComponent != null) {
+ const newComponent = { ...oldComponent, key: action.newKey };
+ return {
+ ...omit(state, action.key),
+ [action.newKey]: newComponent
+ };
+ }
+ }
+
+ return state;
+};
+
+export default components;
+
+export const getComponentByKey = (state, key) =>
+ state[key];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js
new file mode 100644
index 00000000000..0b55882cf2d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import { RECEIVE_PROJECT_MODULES, CHANGE_KEY } from './actions';
+
+const modulesByProject = (state = {}, action = {}) => {
+ if (action.type === RECEIVE_PROJECT_MODULES) {
+ const moduleKeys = action.modules.map(module => module.key);
+ return { ...state, [action.projectKey]: moduleKeys };
+ }
+
+ if (action.type === CHANGE_KEY) {
+ const newState = {};
+ Object.keys(state).forEach(projectKey => {
+ const moduleKeys = state[projectKey];
+ const changedKeyIndex = moduleKeys.indexOf(action.key);
+ if (changedKeyIndex !== -1) {
+ const newModuleKeys = [...moduleKeys];
+ newModuleKeys.splice(changedKeyIndex, 1, action.newKey);
+ newState[projectKey] = newModuleKeys;
+ } else {
+ newState[projectKey] = moduleKeys;
+ }
+ });
+ return newState;
+ }
+
+ return state;
+};
+
+export default modulesByProject;
+
+export const getProjectModules = (state, projectKey) =>
+ state[projectKey];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
index 513e39dc67d..a5c56087ce9 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
@@ -27,6 +27,12 @@ import gates, { getAllGates as nextGetAllGates, getGate } from './gates';
import gateByProject, { getProjectGate as nextGetProjectGate } from './gateByProject';
import links, { getLink } from './links';
import linksByProject, { getLinks } from './linksByProject';
+import components, {
+ getComponentByKey as nextGetComponentByKey
+} from './components';
+import modulesByProject, {
+ getProjectModules as nextGetProjectModules
+} from './modulesByProject';
import globalMessages, {
getGlobalMessages as nextGetGlobalMessages
} from '../../../components/store/globalMessages';
@@ -38,6 +44,8 @@ const rootReducer = combineReducers({
gateByProject,
links,
linksByProject,
+ components,
+ modulesByProject,
globalMessages
});
@@ -69,5 +77,16 @@ export const getProjectLinks = (state, projectKey) =>
getLinks(state.linksByProject, projectKey)
.map(linkId => getLinkById(state, linkId));
+export const getComponentByKey = (state, componentKey) =>
+ nextGetComponentByKey(state.components, componentKey);
+
+export const getProjectModules = (state, projectKey) => {
+ const moduleKeys = nextGetProjectModules(state.modulesByProject, projectKey);
+ if (moduleKeys == null) {
+ return null;
+ }
+ return moduleKeys.map(moduleKey => getComponentByKey(state, moduleKey));
+};
+
export const getGlobalMessages = state =>
nextGetGlobalMessages(state.globalMessages);
diff --git a/server/sonar-web/src/main/less/style.less b/server/sonar-web/src/main/less/style.less
index 26ca85df14c..5743af7c82e 100644
--- a/server/sonar-web/src/main/less/style.less
+++ b/server/sonar-web/src/main/less/style.less
@@ -394,6 +394,7 @@ ul.bullet li {
margin: 0 1px 0 0;
padding: 1px 5px;
.link-no-underline;
+ transition: none;
}
.tabs2 li a.selected, .tabs li a.selected, .tabs .ui-tabs-active a {
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
index d4d40889d91..a3dad4b490a 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb
@@ -64,68 +64,6 @@ class ProjectController < ApplicationController
def key
@project = get_current_project(params[:id])
- @snapshot = @project.last_snapshot
- end
-
- def update_key
- project = get_current_project(params[:id])
-
- new_key = params[:new_key].strip
- if new_key.blank?
- flash[:error] = message('update_key.new_key_cant_be_blank_for_x', :params => project.key)
- elsif new_key == project.key
- flash[:warning] = message('update_key.same_key_for_x', :params => project.key)
- elsif Project.by_key(new_key)
- flash[:error] = message('update_key.cant_update_x_because_resource_already_exist_with_key_x', :params => [project.key, new_key])
- else
- call_backend do
- Internal.component_api.updateKey(project.key, new_key)
- flash[:notice] = message('update_key.key_updated')
- end
- end
-
- redirect_to :action => 'key', :id => project.root_project.id
- end
-
- def prepare_key_bulk_update
- @project = get_current_project(params[:id])
-
- @string_to_replace = params[:string_to_replace].strip
- @replacement_string = params[:replacement_string].strip
- if @string_to_replace.blank? || @replacement_string.blank?
- flash[:error] = message('update_key.fieds_cant_be_blank_for_bulk_update')
- redirect_to :action => 'key', :id => @project.id
- else
- call_backend do
- @key_check_results = Internal.component_api.checkModuleKeysBeforeRenaming(@project.key, @string_to_replace, @replacement_string)
- @can_update = false
- @duplicate_key_found = false
- @key_check_results.each do |key, value|
- if value=="#duplicate_key#"
- @duplicate_key_found = true
- else
- @can_update = true
- end
- end
- @can_update = false if @duplicate_key_found
- end
- end
- end
-
- def perform_key_bulk_update
- project = get_current_project(params[:id])
-
- string_to_replace = params[:string_to_replace].strip
- replacement_string = params[:replacement_string].strip
-
- unless string_to_replace.blank? || replacement_string.blank?
- call_backend do
- Internal.component_api.bulkUpdateKey(project.key, string_to_replace, replacement_string)
- flash[:notice] = message('update_key.key_updated')
- end
- end
-
- redirect_to :action => 'key', :id => project.id
end
def history
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb
deleted file mode 100644
index 471065cbd2e..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_key_modules.html.erb
+++ /dev/null
@@ -1,18 +0,0 @@
- <tr class="<%= cycle 'even', 'odd', :name => 'modules_tree' -%>">
- <td class="thin nowrap" style="padding-left: <%= 3+ module_depth*15 -%>px">
- <%= h(current_module.key) -%>
- </td>
- <td class="thin nowrap">
- <% form_tag( {:action => 'update_key', :id => current_module.id }, :onsubmit => "update_launched();$j('#loading_#{id_prefix}').show();") do -%>
- <input type="text" value="<%= h(current_module.key) -%>" name="new_key" id="key_<%= id_prefix -%>" size="80" maxlength="400">
- <%= submit_tag message('update_key.rename'), :id => 'update_key_' + id_prefix, :class => 'action',
- :confirm => message('update_key.are_you_sure_to_rename_x', :params => current_module.key) %>
- <a href="#" onclick="$j('#key_<%= id_prefix -%>').val('<%= h(current_module.key) -%>');"><%= message('update_key.reset') -%></a>
- <span class="loading" id="loading_<%= id_prefix -%>" style="display: none; padding: 3px 10px;"></span>
- <% end %>
- </td>
- </tr>
- <% current_module.modules.each_with_index do |sub_module, index| %>
- <%= render :partial => 'key_modules', :locals => {:current_module => sub_module, :module_depth => module_depth+1,
- :id_prefix => id_prefix + (index+1).to_s} -%>
- <% end %>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb
deleted file mode 100644
index 1624108b2d8..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/_prepare_keys.html.erb
+++ /dev/null
@@ -1,19 +0,0 @@
-<%
- future_key = key_check_results[current_module.key]
- if future_key=="#duplicate_key#"
- duplicate_key = true
- future_key = message('update_key.duplicate_key')
- end
-%>
- <tr class="<%= cycle 'even', 'odd', :name => 'modules_tree' -%>">
- <td class="thin nowrap" style="padding-left: <%= 3+ module_depth*15 -%>px;">
- <%= h(current_module.key) -%>
- </td>
- <td class="thin nowrap <%= 'error' if duplicate_key -%>" style="<%= 'font-weight: bold;' if future_key -%>">
- <%= future_key ? future_key : current_module.key -%>
- </td>
- </tr>
- <% current_module.modules.each_with_index do |sub_module, index| %>
- <%= render :partial => 'prepare_keys', :locals => {:current_module => sub_module, :module_depth => module_depth+1,
- :key_check_results => key_check_results} -%>
- <% end %> \ No newline at end of file
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb
index 0ae01cfeea0..e9dd9ae3410 100644
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb
+++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/key.html.erb
@@ -1,71 +1,3 @@
-<%
- if controller.java_facade.getResourceTypeBooleanProperty(@project.qualifier, 'updatable_key')
- has_modules = !@project.modules.empty?
- reset_cycle 'modules_tree'
-%>
-
-<script type="text/javascript">
- function update_launched() {
- $j('input.action').each(function(index,input) {
- input.disabled=true;
- });
- }
-</script>
-
-<div class="page">
- <header class="page-header">
- <h1 class="page-title"><%= message('update_key.page') -%></h1>
- <p class="page-description"><%= message('update_key.page.description') -%></p>
- </header>
-
- <% if has_modules %>
- <h2><%= message('update_key.bulk_update') -%></h2>
- <br/>
- <p>
- <%= message('update_key.bulk_change_description') -%>
- <br/><br/>
- <%= message('update_key.current_key_for_project_x_is_x', :params => [@project.name, @project.key]) -%>
- </p>
- <br/>
- <% form_tag( {:action => 'prepare_key_bulk_update', :id => @project.id }, :onsubmit => "update_launched();$j('#loading_bulk_update').show();") do -%>
- <table>
- <tr>
- <td style="padding-right: 20px"><%= message('update_key.replace') -%>:</td>
- <td><input type="text" value="" name="string_to_replace" id="string_to_replace" size="40" maxlength="400"></td>
- <td class="form-val-note" style="padding-left: 10px;"><%= message('update_key.replace_example') -%></td>
- </tr>
- <tr>
- <td style="padding-right: 20px"><%= message('update_key.by') -%>:</td>
- <td><input type="text" value="" name="replacement_string" id="replacement_string" size="40" maxlength="400"></td>
- <td class="form-val-note" style="padding-left: 10px;"><%= message('update_key.by_example') -%></td>
- </tr>
- <tr>
- <td></td>
- <td style="padding-top: 5px">
- <%= submit_tag message('update_key.rename'), :id => 'bulk_update_button', :class => 'action' -%>
- <span class="loading" id="loading_bulk_update" style="display: none; padding: 3px 10px;"></span>
- </td>
- <td></td>
- </tr>
- </table>
- <% end %>
- <br/>
- <br/>
- <h2><%= message('update_key.fine_grained_key_update') -%></h2>
- <br/>
- <% end %>
-
- <table class="data" style="width:1%">
- <thead>
- <tr>
- <th class="nowrap"><%= message('update_key.old_key') -%></th>
- <th><%= message('update_key.new_key') -%></th>
- </tr>
- </thead>
- <tbody>
- <%= render :partial => 'key_modules', :locals => {:current_module => @project, :module_depth => 0, :id_prefix => "0"} -%>
- </tbody>
- </table>
-
- <% end %>
-</div>
+<% content_for :extra_script do %>
+ <script src="<%= ApplicationController.root_context -%>/js/bundles/project-admin.js?v=<%= sonar_version -%>"></script>
+<% end %>
diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb
deleted file mode 100644
index 7c5bcc61074..00000000000
--- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/prepare_key_bulk_update.html.erb
+++ /dev/null
@@ -1,61 +0,0 @@
-<%
- if @string_to_replace && @replacement_string
- # validation screen for bulk update
- reset_cycle 'modules_tree'
-%>
-
- <script type="text/javascript">
- function update_launched() {
- $j('input.action').each(function(index,input) {
- input.disabled=true;
- });
- }
- </script>
-
- <div class="page">
- <header class="page-header">
- <h1 class="page-title">
- <%= @can_update ? message('update_key.bulk_update_confirmation_page') : message('update_key.bulk_update_impossible') -%>
- </h1>
- </header>
- <p>
- <% if @can_update %>
- <%= message('update_key.keys_will_be_updated_as_follows') -%>
- <% else %>
- <% if @duplicate_key_found %>
- <%= message('update_key.cant_update_because_duplicate_keys', :params => [@string_to_replace, @replacement_string]) -%>
- <% else %>
- <%= message('update_key.no_key_to_update', :params => @string_to_replace) -%>
- <% end %>
- <% end %>
- </p>
-
- <table class="data" style="width:1%; margin-top: 10px">
- <thead>
- <tr>
- <th><%= message('update_key.old_key') -%></th>
- <th><%= message('update_key.new_key') -%></th>
- </tr>
- </thead>
- <tbody>
- <%= render :partial => 'prepare_keys', :locals => {:current_module => @project, :module_depth => 0, :key_check_results => @key_check_results} -%>
- </tbody>
- </table>
-
- <% if @can_update %>
- <% form_tag( {:action => 'perform_key_bulk_update', :id => @project.id }, :onsubmit => "update_launched();$j('#loading_bulk_update').show();") do -%>
- <input type="hidden" value="<%= @project.id -%>" name="id" id="project_id">
- <input type="hidden" value="<%= @string_to_replace -%>" name="string_to_replace" id="string_to_replace">
- <input type="hidden" value="<%= @replacement_string -%>" name="replacement_string" id="replacement_string">
- <br/>
- <%= submit_tag message('update_key.rename'), :id => 'bulk_update_button', :class => 'action' -%>
- &nbsp;<a href="<%= url_for :action => 'key', :id => @project.key -%>"><%= message('cancel') -%></a>
- <span class="loading" id="loading_bulk_update" style="display: none; padding: 3px 10px;"></span>
- <% end %>
- <% else %>
- <br/>
- <a href="<%= url_for :action => 'key', :id => @project.key -%>"><%= message('back') -%></a>
- <% end %>
- </div>
-
-<% end %>