diff options
24 files changed, 299 insertions, 927 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkUpdateKeyAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkUpdateKeyAction.java index 60cf61f77f7..bd60c532c11 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkUpdateKeyAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkUpdateKeyAction.java @@ -92,6 +92,7 @@ public class BulkUpdateKeyAction implements ProjectsWsAction { PARAM_DRY_RUN, PARAM_PROJECT, PARAM_FROM, PARAM_TO, PARAM_PROJECT_ID, PARAM_PROJECT) + .setDeprecatedSince("7.6") .setSince("6.1") .setPost(true) .setResponseExample(getClass().getResource("bulk_update_key-example.json")) diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts index efbeedbaa52..60317a41e39 100644 --- a/server/sonar-web/src/main/js/api/components.ts +++ b/server/sonar-web/src/main/js/api/components.ts @@ -222,27 +222,8 @@ export function searchComponents(data?: { return getJSON('/api/components/search', data); } -/** - * Change component's key - */ -export function changeKey(from: string, to: string): Promise<void> { - const url = '/api/projects/update_key'; - const data = { from, to }; - return post(url, data); -} - -/** - * Bulk change component's key - */ -export function bulkChangeKey( - project: string, - from: string, - to: string, - dryRun: boolean = false -): Promise<any> { - const url = '/api/projects/bulk_update_key'; - const data = { project, from, to, dryRun }; - return postJSON(url, data); +export function changeKey(data: { from: string; to: string }) { + return post('/api/projects/update_key', data).catch(throwGlobalError); } export interface SuggestionsResponse { diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index 781120376f3..dca733a3587 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -268,7 +268,7 @@ export default function startReactApp( /> <Route path="project/key" - component={lazyLoad(() => import('../../apps/project-admin/key/Key'))} + component={lazyLoad(() => import('../../apps/projectKey/Key'))} /> </Route> </Route> 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 deleted file mode 100644 index 59ceb9da3b1..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js +++ /dev/null @@ -1,148 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import BulkUpdateForm from './BulkUpdateForm'; -import BulkUpdateResults from './BulkUpdateResults'; -import { reloadUpdateKeyPage } from './utils'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { bulkChangeKey } from '../../../api/components'; -import { parseError } from '../../../helpers/request'; -import { - addGlobalErrorMessage, - addGlobalSuccessMessage, - closeAllGlobalMessages -} from '../../../store/globalMessages'; -import RecentHistory from '../../../app/components/RecentHistory'; - -class BulkUpdate extends React.PureComponent { - static propTypes = { - component: PropTypes.object.isRequired, - addGlobalErrorMessage: PropTypes.func.isRequired, - addGlobalSuccessMessage: PropTypes.func.isRequired, - closeAllGlobalMessages: PropTypes.func.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; - - if (newComponentKey !== component.key) { - RecentHistory.remove(component.key); - } - - this.props.addGlobalSuccessMessage(translate('update_key.key_updated.reload')); - this.setState({ updating: false }); - reloadUpdateKeyPage(newComponentKey); - }) - .catch(e => { - this.setState({ updating: false }); - parseError(e).then(message => this.props.addGlobalErrorMessage(message)); - }); - } - - loadResults(replace, by) { - const { component } = this.props; - bulkChangeKey(component.key, replace, by, true) - .then(r => { - this.setState({ results: r.keys, replace, by }); - this.props.closeAllGlobalMessages(); - }) - .catch(e => { - this.setState({ results: null }); - parseError(e).then(message => this.props.addGlobalErrorMessage(message)); - }); - } - - renderUpdating() { - return ( - <div id="project-key-bulk-update"> - <i className="spinner" /> - </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 - by={by} - onConfirm={this.handleConfirm.bind(this)} - replace={replace} - results={results} - /> - )} - </div> - ); - } -} - -export default connect( - null, - { - addGlobalErrorMessage, - addGlobalSuccessMessage, - closeAllGlobalMessages - } -)(BulkUpdate); 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 deleted file mode 100644 index 07814359462..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { translate } from '../../../helpers/l10n'; -import { SubmitButton } from '../../../components/ui/buttons'; - -export default class BulkUpdateForm extends React.PureComponent { - static propTypes = { - onSubmit: PropTypes.func.isRequired - }; - - handleSubmit = e => { - e.preventDefault(); - - const replace = this.refs.replace.value; - const by = this.refs.by.value; - - this.props.onSubmit(replace, by); - }; - - render() { - return ( - <form onSubmit={this.handleSubmit}> - <div className="modal-field"> - <label htmlFor="bulk-update-replace">{translate('update_key.replace')}</label> - <input - id="bulk-update-replace" - name="replace" - placeholder={translate('update_key.replace_example')} - ref="replace" - required={true} - type="text" - /> - </div> - - <div className="modal-field"> - <label htmlFor="bulk-update-by">{translate('update_key.by')}</label> - <input - id="bulk-update-by" - name="by" - placeholder={translate('update_key.by_example')} - ref="by" - required={true} - type="text" - /> - <SubmitButton className="big-spacer-left" id="bulk-update-see-results"> - {translate('update_key.see_results')} - </SubmitButton> - </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 deleted file mode 100644 index a51599ae9d6..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import { some } from 'lodash'; -import { translateWithParameters, translate } from '../../../helpers/l10n'; -import { Button } from '../../../components/ui/buttons'; - -export default class BulkUpdateResults extends React.PureComponent { - static propTypes = { - results: PropTypes.array.isRequired, - onConfirm: PropTypes.func.isRequired - }; - - 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 className="big-spacer-top" id="bulk-update-simulation"> - {isEmpty && ( - <div className="spacer-bottom" id="bulk-update-nothing"> - {translateWithParameters('update_key.no_key_to_update', replace)} - </div> - )} - - {hasDuplications && ( - <div className="spacer-bottom" id="bulk-update-duplicate"> - {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 className="data zebra zebra-hover" id="bulk-update-results"> - <thead> - <tr> - <th>{translate('update_key.old_key')}</th> - <th>{translate('update_key.new_key')}</th> - </tr> - </thead> - <tbody> - {results.map(result => ( - <tr data-key={result.key} 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.props.onConfirm}> - {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 deleted file mode 100644 index 43e17ebfded..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import React from 'react'; -import UpdateKeyForm from './UpdateKeyForm'; -import QualifierIcon from '../../../components/icons-components/QualifierIcon'; - -export default function FineGrainedUpdate(props) { - const { component, modules } = 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={props.onKeyChange} /> - </td> - </tr> - ))} - </tbody> - </table> - </div> - ); -} 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 deleted file mode 100644 index ee2791f3ded..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js +++ /dev/null @@ -1,156 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import React from 'react'; -import PropTypes from 'prop-types'; -import Helmet from 'react-helmet'; -import { connect } from 'react-redux'; -import Header from './Header'; -import UpdateForm from './UpdateForm'; -import BulkUpdate from './BulkUpdate'; -import FineGrainedUpdate from './FineGrainedUpdate'; -import { reloadUpdateKeyPage } from './utils'; -import { changeKey, fetchProjectModules } from '../store/actions'; -import { translate } from '../../../helpers/l10n'; -import { - addGlobalErrorMessage, - addGlobalSuccessMessage, - closeAllGlobalMessages -} from '../../../store/globalMessages'; -import RecentHistory from '../../../app/components/RecentHistory'; -import { getProjectAdminProjectModules } from '../../../store/rootReducer'; - -class Key extends React.PureComponent { - static propTypes = { - component: PropTypes.object, - fetchProjectModules: PropTypes.func.isRequired, - changeKey: PropTypes.func.isRequired, - addGlobalErrorMessage: PropTypes.func.isRequired, - addGlobalSuccessMessage: PropTypes.func.isRequired, - closeAllGlobalMessages: PropTypes.func.isRequired - }; - - state = { - tab: 'bulk' - }; - - componentDidMount() { - this.props.fetchProjectModules(this.props.component.key); - } - - handleChangeKey = (key, newKey) => { - return this.props.changeKey(key, newKey).then(() => { - if (key === this.props.component.key) { - this.props.addGlobalSuccessMessage(translate('update_key.key_updated.reload')); - RecentHistory.remove(key); - reloadUpdateKeyPage(newKey); - } else { - this.props.addGlobalSuccessMessage(translate('update_key.key_updated')); - } - }); - }; - - handleChangeTab = event => { - event.preventDefault(); - event.currentTarget.blur(); - const { tab } = event.currentTarget.dataset; - this.setState({ tab }); - this.props.closeAllGlobalMessages(); - }; - - 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 className="page page-limited" id="project-key"> - <Helmet title={translate('update_key.page')} /> - <Header /> - - {modules == null && <i className="spinner" />} - - {noModules && ( - <div> - <UpdateForm component={component} onKeyChange={this.handleChangeKey} /> - </div> - )} - - {hasModules && ( - <div className="boxed-group boxed-group-inner"> - <div className="big-spacer-bottom"> - <ul className="tabs"> - <li> - <a - className={tab === 'bulk' ? 'selected' : ''} - data-tab="bulk" - href="#" - id="update-key-tab-bulk" - onClick={this.handleChangeTab}> - {translate('update_key.bulk_update')} - </a> - </li> - <li> - <a - className={tab === 'fine' ? 'selected' : ''} - data-tab="fine" - href="#" - id="update-key-tab-fine" - onClick={this.handleChangeTab}> - {translate('update_key.fine_grained_key_update')} - </a> - </li> - </ul> - </div> - - {tab === 'bulk' && <BulkUpdate component={component} />} - - {tab === 'fine' && ( - <FineGrainedUpdate - component={component} - modules={modules} - onError={this.props.addGlobalErrorMessage} - onKeyChange={this.handleChangeKey} - onSuccess={this.props.closeAllGlobalMessages} - /> - )} - </div> - )} - </div> - ); - } -} - -const mapStateToProps = (state, ownProps) => ({ - modules: getProjectAdminProjectModules(state, ownProps.location.query.id) -}); - -export default connect( - mapStateToProps, - { - fetchProjectModules, - changeKey, - addGlobalErrorMessage, - addGlobalSuccessMessage, - closeAllGlobalMessages - } -)(Key); diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyConfirm.tsx b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyConfirm.tsx deleted file mode 100644 index 8796aac4386..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyConfirm.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import * as React from 'react'; -import ConfirmButton, { ChildrenProps } from '../../../components/controls/ConfirmButton'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; - -interface Props { - children: (props: ChildrenProps) => React.ReactNode; - component: { key: string; name: string }; - newKey: string | undefined; - onConfirm: (oldKey: string, newKey: string) => Promise<void>; -} - -export default class UpdateKeyConfirm extends React.PureComponent<Props> { - handleConfirm = () => { - return this.props.newKey - ? this.props.onConfirm(this.props.component.key, this.props.newKey) - : Promise.reject(undefined); - }; - - render() { - const { children, component, newKey } = this.props; - - return ( - <ConfirmButton - confirmButtonText={translate('update_verb')} - modalBody={ - <> - {translateWithParameters('update_key.are_you_sure_to_change_key', component.name)} - <div className="spacer-top"> - {translate('update_key.old_key')} - {': '} - <strong>{component.key}</strong> - </div> - <div className="spacer-top"> - {translate('update_key.new_key')} - {': '} - <strong>{newKey}</strong> - </div> - </> - } - modalHeader={translate('update_key.page')} - onConfirm={this.handleConfirm}> - {children} - </ConfirmButton> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.tsx b/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.tsx deleted file mode 100644 index 6d8a023ec03..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import * as React from 'react'; -import UpdateKeyConfirm from './UpdateKeyConfirm'; -import { Button } from '../../../components/ui/buttons'; -import { translate } from '../../../helpers/l10n'; - -interface Props { - component: { key: string; name: string }; - onKeyChange: (oldKey: string, newKey: string) => Promise<void>; -} - -interface State { - newKey?: string; -} - -export default class UpdateKeyForm extends React.PureComponent<Props, State> { - state: State = {}; - - handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { - const newKey = event.currentTarget.value; - this.setState({ newKey }); - }; - - handleResetClick = () => { - this.setState({ newKey: undefined }); - }; - - render() { - const { component } = this.props; - const { newKey } = this.state; - const value = newKey !== undefined ? newKey : component.key; - const hasChanged = newKey !== undefined && newKey !== component.key; - - return ( - <div className="js-fine-grained-update" data-key={component.key}> - <input - className="input-super-large big-spacer-right" - onChange={this.handleInputChange} - type="text" - value={value} - /> - - <UpdateKeyConfirm component={component} newKey={newKey} onConfirm={this.props.onKeyChange}> - {({ onClick }) => ( - <Button disabled={!hasChanged} onClick={onClick}> - {translate('update_verb')} - </Button> - )} - </UpdateKeyConfirm> - - <Button className="spacer-left" disabled={!hasChanged} onClick={this.handleResetClick}> - {translate('reset_verb')} - </Button> - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/utils.js b/server/sonar-web/src/main/js/apps/project-admin/key/utils.js deleted file mode 100644 index 0a30626afa5..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/key/utils.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -export const reloadUpdateKeyPage = componentKey => { - setTimeout(() => { - window.location = window.baseUrl + '/project/key?id=' + encodeURIComponent(componentKey); - }, 3000); -}; 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 deleted file mode 100644 index cdb042caee9..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import { getTree, changeKey as changeKeyApi } from '../../../api/components'; -import throwGlobalError from '../../../app/utils/throwGlobalError'; - -export const RECEIVE_PROJECT_MODULES = 'projectAdmin/RECEIVE_PROJECT_MODULES'; -const receiveProjectModules = (projectKey, modules) => ({ - type: RECEIVE_PROJECT_MODULES, - projectKey, - modules -}); - -export const fetchProjectModules = projectKey => dispatch => { - getTree({ component: projectKey, qualifiers: 'BRC', s: 'name', ps: 500 }).then( - r => { - dispatch(receiveProjectModules(projectKey, r.components)); - }, - () => {} - ); -}; - -export const CHANGE_KEY = 'projectAdmin/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)), - throwGlobalError - ); -}; 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 deleted file mode 100644 index d8e238365db..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/store/components.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import { keyBy, omit } from 'lodash'; -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 deleted file mode 100644 index 9b5014be2d1..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -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 deleted file mode 100644 index 6bff2faed22..00000000000 --- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2019 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. - */ -import { combineReducers } from 'redux'; -import components, { getComponentByKey as nextGetComponentByKey } from './components'; -import modulesByProject, { getProjectModules as nextGetProjectModules } from './modulesByProject'; - -const rootReducer = combineReducers({ - components, - modulesByProject -}); - -export default rootReducer; - -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)); -}; diff --git a/server/sonar-web/src/main/js/apps/projectKey/Key.tsx b/server/sonar-web/src/main/js/apps/projectKey/Key.tsx new file mode 100644 index 00000000000..38454a5bc00 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectKey/Key.tsx @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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. + */ +import * as React from 'react'; +import Helmet from 'react-helmet'; +import { withRouter, WithRouterProps } from 'react-router'; +import UpdateForm from './UpdateForm'; +import { changeKey } from '../../api/components'; +import RecentHistory from '../../app/components/RecentHistory'; +import { translate } from '../../helpers/l10n'; + +interface Props { + component: Pick<T.Component, 'key' | 'name'>; +} + +export class Key extends React.PureComponent<Props & WithRouterProps> { + handleChangeKey = (newKey: string) => { + return changeKey({ from: this.props.component.key, to: newKey }).then(() => { + RecentHistory.remove(this.props.component.key); + this.props.router.replace({ pathname: '/project/key', query: { id: newKey } }); + }); + }; + + render() { + const { component } = this.props; + return ( + <div className="page page-limited" id="project-key"> + <Helmet title={translate('update_key.page')} /> + <header className="page-header"> + <h1 className="page-title">{translate('update_key.page')}</h1> + <div className="page-description">{translate('update_key.page.description')}</div> + </header> + <UpdateForm component={component} onKeyChange={this.handleChangeKey} /> + </div> + ); + } +} + +export default withRouter(Key); diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.tsx b/server/sonar-web/src/main/js/apps/projectKey/UpdateForm.tsx index 1ece13fef36..39dbe961d25 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.tsx +++ b/server/sonar-web/src/main/js/apps/projectKey/UpdateForm.tsx @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import UpdateKeyConfirm from './UpdateKeyConfirm'; -import { Button, SubmitButton } from '../../../components/ui/buttons'; -import { translate } from '../../../helpers/l10n'; +import { Button, SubmitButton } from '../../components/ui/buttons'; +import { translate, translateWithParameters } from '../../helpers/l10n'; +import ConfirmButton from '../../components/controls/ConfirmButton'; interface Props { - component: { key: string; name: string }; - onKeyChange: (oldKey: string, newKey: string) => Promise<void>; + component: Pick<T.Component, 'key' | 'name'>; + onKeyChange: (newKey: string) => Promise<void>; } interface State { @@ -50,7 +50,26 @@ export default class UpdateForm extends React.PureComponent<Props, State> { const hasChanged = value !== component.key; return ( - <UpdateKeyConfirm component={component} newKey={newKey} onConfirm={this.props.onKeyChange}> + <ConfirmButton + confirmButtonText={translate('update_verb')} + confirmData={newKey} + modalBody={ + <> + {translateWithParameters('update_key.are_you_sure_to_change_key', component.name)} + <div className="spacer-top"> + {translate('update_key.old_key')} + {': '} + <strong>{component.key}</strong> + </div> + <div className="spacer-top"> + {translate('update_key.new_key')} + {': '} + <strong>{newKey}</strong> + </div> + </> + } + modalHeader={translate('update_key.page')} + onConfirm={this.props.onKeyChange}> {({ onFormSubmit }) => ( <form onSubmit={onFormSubmit}> <input @@ -79,7 +98,7 @@ export default class UpdateForm extends React.PureComponent<Props, State> { </div> </form> )} - </UpdateKeyConfirm> + </ConfirmButton> ); } } diff --git a/server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx b/server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx new file mode 100644 index 00000000000..57c8aa569b4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { WithRouterProps } from 'react-router'; +import { Key } from '../Key'; +import { changeKey } from '../../../api/components'; + +jest.mock('../../../api/components', () => ({ + changeKey: jest.fn().mockResolvedValue(undefined) +})); + +it('should render and change key', async () => { + const withRouterProps = { router: { replace: jest.fn() } as any } as WithRouterProps; + const wrapper = shallow(<Key component={{ key: 'foo', name: 'Foo' }} {...withRouterProps} />); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('UpdateForm').prop<Function>('onKeyChange')('bar'); + await new Promise(setImmediate); + expect(changeKey).toBeCalledWith({ from: 'foo', to: 'bar' }); + expect(withRouterProps.router.replace).toBeCalledWith({ + pathname: '/project/key', + query: { id: 'bar' } + }); +}); diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Header.js b/server/sonar-web/src/main/js/apps/projectKey/__tests__/UpdateForm-test.tsx index 6c14beda91b..457575b7abf 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/key/Header.js +++ b/server/sonar-web/src/main/js/apps/projectKey/__tests__/UpdateForm-test.tsx @@ -17,14 +17,25 @@ * 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'; +import * as React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import UpdateForm from '../UpdateForm'; +import { change, click } from '../../../helpers/testUtils'; -export default function Header() { - 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> +it('should render', () => { + const wrapper = shallow( + <UpdateForm component={{ key: 'foo', name: 'Foo' }} onKeyChange={jest.fn()} /> ); + expect(getInner(wrapper)).toMatchSnapshot(); + + change(getInner(wrapper).find('input'), 'bar'); + expect(getInner(wrapper)).toMatchSnapshot(); + + click(getInner(wrapper).find('Button')); + expect(getInner(wrapper)).toMatchSnapshot(); +}); + +function getInner(wrapper: ShallowWrapper) { + // TODO find a better way to do this + return wrapper.dive().dive(); } diff --git a/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap new file mode 100644 index 00000000000..da5365e6d5c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render and change key 1`] = ` +<div + className="page page-limited" + id="project-key" +> + <HelmetWrapper + defer={true} + encodeSpecialCharacters={true} + title="update_key.page" + /> + <header + className="page-header" + > + <h1 + className="page-title" + > + update_key.page + </h1> + <div + className="page-description" + > + update_key.page.description + </div> + </header> + <UpdateForm + component={ + Object { + "key": "foo", + "name": "Foo", + } + } + onKeyChange={[Function]} + /> +</div> +`; diff --git a/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/UpdateForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/UpdateForm-test.tsx.snap new file mode 100644 index 00000000000..0a009777e05 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/UpdateForm-test.tsx.snap @@ -0,0 +1,112 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +<Fragment> + <form + onSubmit={[Function]} + > + <input + className="input-super-large" + id="update-key-new-key" + onChange={[Function]} + placeholder="update_key.new_key" + required={true} + type="text" + value="foo" + /> + <div + className="spacer-top" + > + <SubmitButton + disabled={true} + id="update-key-submit" + > + update_verb + </SubmitButton> + <Button + className="spacer-left" + disabled={true} + id="update-key-reset" + onClick={[Function]} + type="reset" + > + reset_verb + </Button> + </div> + </form> +</Fragment> +`; + +exports[`should render 2`] = ` +<Fragment> + <form + onSubmit={[Function]} + > + <input + className="input-super-large" + id="update-key-new-key" + onChange={[Function]} + placeholder="update_key.new_key" + required={true} + type="text" + value="bar" + /> + <div + className="spacer-top" + > + <SubmitButton + disabled={false} + id="update-key-submit" + > + update_verb + </SubmitButton> + <Button + className="spacer-left" + disabled={false} + id="update-key-reset" + onClick={[Function]} + type="reset" + > + reset_verb + </Button> + </div> + </form> +</Fragment> +`; + +exports[`should render 3`] = ` +<Fragment> + <form + onSubmit={[Function]} + > + <input + className="input-super-large" + id="update-key-new-key" + onChange={[Function]} + placeholder="update_key.new_key" + required={true} + type="text" + value="foo" + /> + <div + className="spacer-top" + > + <SubmitButton + disabled={true} + id="update-key-submit" + > + update_verb + </SubmitButton> + <Button + className="spacer-left" + disabled={true} + id="update-key-reset" + onClick={[Function]} + type="reset" + > + reset_verb + </Button> + </div> + </form> +</Fragment> +`; diff --git a/server/sonar-web/src/main/js/components/SelectList/SelectListListContainer.tsx b/server/sonar-web/src/main/js/components/SelectList/SelectListListContainer.tsx index 96d969a33f7..829bee3f284 100644 --- a/server/sonar-web/src/main/js/components/SelectList/SelectListListContainer.tsx +++ b/server/sonar-web/src/main/js/components/SelectList/SelectListListContainer.tsx @@ -91,7 +91,7 @@ export default class SelectListListContainer extends React.PureComponent<Props, onCheck={this.handleBulkChange} thirdState={selectedElements.length > 0 && elements.length !== selectedElements.length}> <span className="big-spacer-left"> - {translate('update_key.bulk_update')} + {translate('bulk_change')} <DeferredSpinner className="spacer-left" loading={this.state.loading} timeout={10} /> </span> </Checkbox> diff --git a/server/sonar-web/src/main/js/store/rootReducer.ts b/server/sonar-web/src/main/js/store/rootReducer.ts index fa3890586fc..e4286c79a99 100644 --- a/server/sonar-web/src/main/js/store/rootReducer.ts +++ b/server/sonar-web/src/main/js/store/rootReducer.ts @@ -24,7 +24,6 @@ import languages, * as fromLanguages from './languages'; import metrics, * as fromMetrics from './metrics'; import organizations, * as fromOrganizations from './organizations'; import users, * as fromUsers from './users'; -import projectAdminApp, * as fromProjectAdminApp from '../apps/project-admin/store/rootReducer'; import settingsApp, * as fromSettingsApp from '../apps/settings/store/rootReducer'; export type Store = { @@ -36,7 +35,6 @@ export type Store = { users: fromUsers.State; // apps - projectAdminApp: any; settingsApp: any; }; @@ -49,7 +47,6 @@ export default combineReducers<Store>({ users, // apps - projectAdminApp, settingsApp }); @@ -128,7 +125,3 @@ export function isSettingsAppLoading(state: Store, key: string) { export function getSettingsAppValidationMessage(state: Store, key: string) { return fromSettingsApp.getValidationMessage(state.settingsApp, key); } - -export function getProjectAdminProjectModules(state: Store, projectKey: string) { - return fromProjectAdminApp.getProjectModules(state.projectAdminApp, projectKey); -} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index ef551894203..dd4cb444f2d 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -514,7 +514,7 @@ project_quality_profiles.page.description=Choose which profile is associated wit project_quality_gate.page=Quality Gate project_quality_gate.page.description=Choose which quality gate is associated with this project. update_key.page=Update Key -update_key.page.description=Edit the keys of a project and/or its modules. Key changes must be made here BEFORE analyzing the project with the new keys, otherwise the analysis will simply create another project with the new key, rather than updating the existing project. +update_key.page.description=Edit the key of a project. Key changes must be made here BEFORE analyzing the project with the new keys, otherwise the analysis will simply create another project with the new key, rather than updating the existing project. deletion.page=Deletion project_deletion.page.description=Delete this project. The operation cannot be undone. portfolio_deletion.page.description=This portfolio and its sub-portfolios will be deleted. If this portfolio is referenced by other entities, it will be removed from them. Independent entities referenced by this portfolio, such as projects and other top-level portfolios will not be deleted. This operation cannot be undone. @@ -1075,27 +1075,12 @@ project_activity.custom_metric.covered_lines=Covered Lines #------------------------------------------------------------------------------ # -# PROJECT / MODULE "UPDATE KEY" PAGE +# PROJECT "UPDATE KEY" PAGE # #------------------------------------------------------------------------------ -update_key.bulk_update=Bulk Update -update_key.fine_grained_key_update=Fine-grained Update update_key.old_key=Old Key update_key.new_key=New Key -update_key.key_updated=The key has successfully been updated for all required resources. -update_key.key_updated.reload=The key has successfully been updated for all required resources. This page will be reloaded shortly. -update_key.bulk_change_description=The bulk update allows to replace a part of the current key(s) by another string on the current project and all its submodules - if applicable. -update_key.current_key_for_project_x_is_x=The key of the "{0}" project is currently "{1}". -update_key.replace=Replace -update_key.by=By -update_key.replace_example=org.myCompany -update_key.by_example=com.myNewCompany -update_key.cant_update_because_duplicate_keys=The replacement of "{0}" by "{1}" is impossible as it would result in duplicate keys (in red below): -update_key.keys_will_be_updated_as_follows=The resources will be updated as follows: -update_key.duplicate_key=Duplicate Key -update_key.no_key_to_update=No key contains the string to replace ("{0}"). -update_key.are_you_sure_to_change_key=Are you sure you want to change key of "{0}", as well as all its modules and resources? -update_key.see_results=See Results +update_key.are_you_sure_to_change_key=Are you sure you want to change key of "{0}"? #------------------------------------------------------------------------------ |