@@ -20,82 +20,86 @@ | |||
package org.sonarqube.qa.util.pageobjects; | |||
import com.codeborne.selenide.Condition; | |||
import com.codeborne.selenide.Selenide; | |||
import com.codeborne.selenide.SelenideElement; | |||
import static com.codeborne.selenide.Condition.visible; | |||
import static com.codeborne.selenide.Selenide.$; | |||
public class ProjectKeyPage { | |||
public ProjectKeyPage() { | |||
Selenide.$("#project-key").should(Condition.exist); | |||
$("#project-key").should(Condition.exist); | |||
} | |||
public ProjectKeyPage assertSimpleUpdate() { | |||
Selenide.$("#update-key-new-key").shouldBe(Condition.visible); | |||
Selenide.$("#update-key-submit").shouldBe(Condition.visible); | |||
$("#update-key-new-key").shouldBe(visible); | |||
$("#update-key-submit").shouldBe(visible); | |||
return this; | |||
} | |||
public ProjectKeyPage trySimpleUpdate(String newKey) { | |||
Selenide.$("#update-key-new-key").val(newKey); | |||
Selenide.$("#update-key-submit").click(); | |||
Selenide.$("#update-key-confirm").click(); | |||
$("#update-key-new-key").val(newKey); | |||
$("#update-key-submit").click(); | |||
$(".modal").shouldBe(visible); | |||
$(".modal button[type=\"submit\"]").click(); | |||
return this; | |||
} | |||
public ProjectKeyPage openFineGrainedUpdate() { | |||
Selenide.$("#update-key-tab-fine").click(); | |||
Selenide.$("#project-key-fine-grained-update").shouldBe(Condition.visible); | |||
$("#update-key-tab-fine").click(); | |||
$("#project-key-fine-grained-update").shouldBe(visible); | |||
return this; | |||
} | |||
public ProjectKeyPage tryFineGrainedUpdate(String key, String newKey) { | |||
SelenideElement form = Selenide.$(".js-fine-grained-update[data-key=\"" + key + "\"]"); | |||
form.shouldBe(Condition.visible); | |||
SelenideElement form = $(".js-fine-grained-update[data-key=\"" + key + "\"]"); | |||
form.shouldBe(visible); | |||
form.$("input").val(newKey); | |||
form.$("button").click(); | |||
Selenide.$("#update-key-confirm").click(); | |||
$(".modal").shouldBe(visible); | |||
$(".modal button[type=\"submit\"]").click(); | |||
return this; | |||
} | |||
public ProjectKeyPage assertBulkChange() { | |||
Selenide.$("#bulk-update-replace").shouldBe(Condition.visible); | |||
Selenide.$("#bulk-update-by").shouldBe(Condition.visible); | |||
Selenide.$("#bulk-update-see-results").shouldBe(Condition.visible); | |||
$("#bulk-update-replace").shouldBe(visible); | |||
$("#bulk-update-by").shouldBe(visible); | |||
$("#bulk-update-see-results").shouldBe(visible); | |||
return this; | |||
} | |||
public ProjectKeyPage simulateBulkChange(String replace, String by) { | |||
Selenide.$("#bulk-update-replace").val(replace); | |||
Selenide.$("#bulk-update-by").val(by); | |||
Selenide.$("#bulk-update-see-results").click(); | |||
$("#bulk-update-replace").val(replace); | |||
$("#bulk-update-by").val(by); | |||
$("#bulk-update-see-results").click(); | |||
Selenide.$("#bulk-update-simulation").shouldBe(Condition.visible); | |||
$("#bulk-update-simulation").shouldBe(visible); | |||
return this; | |||
} | |||
public ProjectKeyPage assertBulkChangeSimulationResult(String oldKey, String newKey) { | |||
SelenideElement row = Selenide.$("#bulk-update-results").$("[data-key=\"" + oldKey + "\"]"); | |||
SelenideElement row = $("#bulk-update-results").$("[data-key=\"" + oldKey + "\"]"); | |||
row.$(".js-old-key").should(Condition.text(oldKey)); | |||
row.$(".js-new-key").should(Condition.text(newKey)); | |||
return this; | |||
} | |||
public ProjectKeyPage assertDuplicated(String oldKey) { | |||
SelenideElement row = Selenide.$("#bulk-update-results").$("[data-key=\"" + oldKey + "\"]"); | |||
row.$(".js-new-key").$(".badge-danger").shouldBe(Condition.visible); | |||
SelenideElement row = $("#bulk-update-results").$("[data-key=\"" + oldKey + "\"]"); | |||
row.$(".js-new-key").$(".badge-danger").shouldBe(visible); | |||
return this; | |||
} | |||
public ProjectKeyPage confirmBulkUpdate() { | |||
Selenide.$("#bulk-update-confirm").click(); | |||
$("#bulk-update-confirm").click(); | |||
return this; | |||
} | |||
public ProjectKeyPage assertSuccessfulBulkUpdate() { | |||
Selenide.$(".process-spinner") | |||
.shouldBe(Condition.visible) | |||
$(".process-spinner") | |||
.shouldBe(visible) | |||
.shouldHave(Condition.text("The key has successfully been updated for all required resources")); | |||
return this; | |||
} |
@@ -17,30 +17,21 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getJSON, post, postJSON } from '../helpers/request'; | |||
import { ProjectLink } from '../app/types'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
export interface ProjectLink { | |||
id: string; | |||
name: string; | |||
type: string; | |||
url: string; | |||
} | |||
import { getJSON, post, postJSON } from '../helpers/request'; | |||
export function getProjectLinks(projectKey: string): Promise<ProjectLink[]> { | |||
const url = '/api/project_links/search'; | |||
const data = { projectKey }; | |||
return getJSON(url, data).then(r => r.links, throwGlobalError); | |||
return getJSON('/api/project_links/search', { projectKey }).then(r => r.links, throwGlobalError); | |||
} | |||
export function deleteLink(linkId: string): Promise<void> { | |||
const url = '/api/project_links/delete'; | |||
const data = { id: linkId }; | |||
return post(url, data); | |||
export function deleteLink(linkId: string) { | |||
return post('/api/project_links/delete', { id: linkId }).catch(throwGlobalError); | |||
} | |||
export function createLink(projectKey: string, name: string, url: string): Promise<any> { | |||
const apiURL = '/api/project_links/create'; | |||
const data = { projectKey, name, url }; | |||
return postJSON(apiURL, data).then(r => r.link); | |||
export function createLink(projectKey: string, name: string, url: string): Promise<ProjectLink> { | |||
return postJSON('/api/project_links/create', { projectKey, name, url }).then( | |||
r => r.link, | |||
throwGlobalError | |||
); | |||
} |
@@ -245,6 +245,13 @@ export interface PermissionTemplate { | |||
}>; | |||
} | |||
export interface ProjectLink { | |||
id: string; | |||
name: string; | |||
type: string; | |||
url: string; | |||
} | |||
export interface Rule { | |||
isTemplate?: boolean; | |||
key: string; |
@@ -33,7 +33,6 @@ import Modal from '../../components/controls/Modal'; | |||
import SearchBox from '../../components/controls/SearchBox'; | |||
import Select from '../../components/controls/Select'; | |||
import Tooltip from '../../components/controls/Tooltip'; | |||
import ModalForm from '../../components/common/modal-form'; | |||
import SelectList from '../../components/SelectList'; | |||
import CoverageRating from '../../components/ui/CoverageRating'; | |||
import DuplicationsRating from '../../components/ui/DuplicationsRating'; | |||
@@ -63,9 +62,7 @@ const exposeLibraries = () => { | |||
Tooltip, | |||
Select, | |||
SelectList, | |||
SearchBox, | |||
// deprecated, used in Governance | |||
ModalForm_deprecated: ModalForm | |||
SearchBox | |||
}; | |||
}; | |||
@@ -19,8 +19,8 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { isProvided, getLinkName } from '../../project-admin/links/utils'; | |||
import { ProjectLink } from '../../../app/types'; | |||
import BugTrackerIcon from '../../../components/ui/BugTrackerIcon'; | |||
import { ProjectLink } from '../../../api/projectLinks'; | |||
interface Props { | |||
link: ProjectLink; |
@@ -19,9 +19,9 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import MetaLink from './MetaLink'; | |||
import { getProjectLinks, ProjectLink } from '../../../api/projectLinks'; | |||
import { getProjectLinks } from '../../../api/projectLinks'; | |||
import { orderLinks } from '../../project-admin/links/utils'; | |||
import { LightComponent } from '../../../app/types'; | |||
import { LightComponent, ProjectLink } from '../../../app/types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { |
@@ -26,14 +26,13 @@ import UpdateForm from './UpdateForm'; | |||
import BulkUpdate from './BulkUpdate'; | |||
import FineGrainedUpdate from './FineGrainedUpdate'; | |||
import { reloadUpdateKeyPage } from './utils'; | |||
import { fetchProjectModules, changeKey } from '../store/actions'; | |||
import { changeKey, fetchProjectModules } from '../store/actions'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { | |||
addGlobalErrorMessage, | |||
closeAllGlobalMessages, | |||
addGlobalSuccessMessage | |||
addGlobalSuccessMessage, | |||
closeAllGlobalMessages | |||
} from '../../../store/globalMessages/duck'; | |||
import { parseError } from '../../../helpers/request'; | |||
import RecentHistory from '../../../app/components/RecentHistory'; | |||
import { getProjectAdminProjectModules } from '../../../store/rootReducer'; | |||
@@ -55,29 +54,25 @@ class Key extends React.PureComponent { | |||
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')); | |||
} | |||
}) | |||
.catch(e => { | |||
parseError(e).then(this.props.addGlobalErrorMessage); | |||
}); | |||
} | |||
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(tab, e) { | |||
e.preventDefault(); | |||
e.target.blur(); | |||
handleChangeTab = event => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
const { tab } = event.currentTarget.dataset; | |||
this.setState({ tab }); | |||
this.props.closeAllGlobalMessages(); | |||
} | |||
}; | |||
render() { | |||
const { component, modules } = this.props; | |||
@@ -88,7 +83,7 @@ class Key extends React.PureComponent { | |||
const { tab } = this.state; | |||
return ( | |||
<div id="project-key" className="page page-limited"> | |||
<div className="page page-limited" id="project-key"> | |||
<Helmet title={translate('update_key.page')} /> | |||
<Header /> | |||
@@ -96,7 +91,7 @@ class Key extends React.PureComponent { | |||
{noModules && ( | |||
<div> | |||
<UpdateForm component={component} onKeyChange={this.handleChangeKey.bind(this)} /> | |||
<UpdateForm component={component} onKeyChange={this.handleChangeKey} /> | |||
</div> | |||
)} | |||
@@ -106,19 +101,21 @@ class Key extends React.PureComponent { | |||
<ul className="tabs"> | |||
<li> | |||
<a | |||
id="update-key-tab-bulk" | |||
className={tab === 'bulk' ? 'selected' : ''} | |||
data-tab="bulk" | |||
href="#" | |||
onClick={this.handleChangeTab.bind(this, 'bulk')}> | |||
id="update-key-tab-bulk" | |||
onClick={this.handleChangeTab}> | |||
{translate('update_key.bulk_update')} | |||
</a> | |||
</li> | |||
<li> | |||
<a | |||
id="update-key-tab-fine" | |||
className={tab === 'fine' ? 'selected' : ''} | |||
data-tab="fine" | |||
href="#" | |||
onClick={this.handleChangeTab.bind(this, 'fine')}> | |||
id="update-key-tab-fine" | |||
onClick={this.handleChangeTab}> | |||
{translate('update_key.fine_grained_key_update')} | |||
</a> | |||
</li> | |||
@@ -131,9 +128,9 @@ class Key extends React.PureComponent { | |||
<FineGrainedUpdate | |||
component={component} | |||
modules={modules} | |||
onKeyChange={this.handleChangeKey.bind(this)} | |||
onSuccess={this.props.closeAllGlobalMessages} | |||
onError={this.props.addGlobalErrorMessage} | |||
onKeyChange={this.handleChangeKey} | |||
onSuccess={this.props.closeAllGlobalMessages} | |||
/> | |||
)} | |||
</div> |
@@ -1,87 +0,0 @@ | |||
/* | |||
* 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 React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import UpdateKeyConfirmation from './views/UpdateKeyConfirmation'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class UpdateForm extends React.PureComponent { | |||
static propTypes = { | |||
component: PropTypes.object.isRequired, | |||
onKeyChange: 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 }); | |||
} | |||
handleReset(e) { | |||
e.preventDefault(); | |||
this.setState({ newKey: null }); | |||
} | |||
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={true} | |||
onChange={this.handleChange.bind(this)} | |||
/> | |||
<div className="spacer-top"> | |||
<button id="update-key-submit" disabled={!hasChanged}> | |||
{translate('update_verb')} | |||
</button>{' '} | |||
<button | |||
id="update-key-reset" | |||
disabled={!hasChanged} | |||
onClick={this.handleReset.bind(this)}> | |||
{translate('reset_verb')} | |||
</button> | |||
</div> | |||
</form> | |||
); | |||
} | |||
} |
@@ -0,0 +1,85 @@ | |||
/* | |||
* 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 UpdateKeyConfirm from './UpdateKeyConfirm'; | |||
import { Button, SubmitButton } 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 UpdateForm extends React.PureComponent<Props, State> { | |||
state: State = {}; | |||
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||
const newKey = event.currentTarget.value; | |||
this.setState({ newKey }); | |||
}; | |||
handleReset = () => { | |||
this.setState({ newKey: undefined }); | |||
}; | |||
render() { | |||
const { component } = this.props; | |||
const { newKey } = this.state; | |||
const value = newKey != null ? newKey : component.key; | |||
const hasChanged = value !== component.key; | |||
return ( | |||
<UpdateKeyConfirm component={component} newKey={newKey} onConfirm={this.props.onKeyChange}> | |||
{({ onFormSubmit }) => ( | |||
<form onSubmit={onFormSubmit}> | |||
<input | |||
className="input-super-large" | |||
id="update-key-new-key" | |||
onChange={this.handleChange} | |||
placeholder={translate('update_key.new_key')} | |||
required={true} | |||
type="text" | |||
value={value} | |||
/> | |||
<div className="spacer-top"> | |||
<SubmitButton disabled={!hasChanged} id="update-key-submit"> | |||
{translate('update_verb')} | |||
</SubmitButton> | |||
<Button | |||
className="spacer-left" | |||
disabled={!hasChanged} | |||
id="update-key-reset" | |||
onClick={this.handleReset} | |||
type="reset"> | |||
{translate('reset_verb')} | |||
</Button> | |||
</div> | |||
</form> | |||
)} | |||
</UpdateKeyConfirm> | |||
); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* 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 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> | |||
); | |||
} | |||
} |
@@ -1,89 +0,0 @@ | |||
/* | |||
* 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 React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import UpdateKeyConfirmation from './views/UpdateKeyConfirmation'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class UpdateKeyForm extends React.PureComponent { | |||
static propTypes = { | |||
component: 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} | |||
/> | |||
<button disabled={!hasChanged} onClick={this.handleUpdateClick}> | |||
{translate('update_verb')} | |||
</button> | |||
<button className="spacer-left" disabled={!hasChanged} onClick={this.handleResetClick}> | |||
{translate('reset_verb')} | |||
</button> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,75 @@ | |||
/* | |||
* 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 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> | |||
); | |||
} | |||
} |
@@ -1,30 +0,0 @@ | |||
<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> |
@@ -1,41 +0,0 @@ | |||
/* | |||
* 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 Template from './UpdateKeyConfirmation.hbs'; | |||
import ModalForm from '../../../../components/common/modal-form'; | |||
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); | |||
this.destroy(); | |||
}, | |||
serializeData() { | |||
return { | |||
component: this.options.component, | |||
newKey: this.options.newKey | |||
}; | |||
} | |||
}); |
@@ -0,0 +1,111 @@ | |||
/* | |||
* 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 DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import SimpleModal from '../../../components/controls/SimpleModal'; | |||
import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
onClose: () => void; | |||
onSubmit: (name: string, url: string) => Promise<void>; | |||
} | |||
interface State { | |||
name: string; | |||
url: string; | |||
} | |||
export default class CreationModal extends React.PureComponent<Props, State> { | |||
state: State = { name: '', url: '' }; | |||
handleSubmit = () => { | |||
return this.props.onSubmit(this.state.name, this.state.url).then(this.props.onClose); | |||
}; | |||
handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||
this.setState({ name: event.currentTarget.value }); | |||
}; | |||
handleUrlChange = (event: React.ChangeEvent<HTMLInputElement>) => { | |||
this.setState({ url: event.currentTarget.value }); | |||
}; | |||
render() { | |||
const header = translate('project_links.create_new_project_link'); | |||
return ( | |||
<SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}> | |||
{({ onCloseClick, onFormSubmit, submitting }) => ( | |||
<form onSubmit={onFormSubmit}> | |||
<header className="modal-head"> | |||
<h2>{header}</h2> | |||
</header> | |||
<div className="modal-body"> | |||
<div className="modal-field"> | |||
<label htmlFor="create-link-name"> | |||
{translate('project_links.name')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-link-name" | |||
maxLength={128} | |||
name="name" | |||
onChange={this.handleNameChange} | |||
required={true} | |||
type="text" | |||
value={this.state.name} | |||
/> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="create-link-url"> | |||
{translate('project_links.url')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
<input | |||
id="create-link-url" | |||
maxLength={128} | |||
name="url" | |||
onChange={this.handleUrlChange} | |||
required={true} | |||
type="text" | |||
value={this.state.url} | |||
/> | |||
</div> | |||
</div> | |||
<footer className="modal-foot"> | |||
<DeferredSpinner className="spacer-right" loading={submitting} /> | |||
<SubmitButton disabled={submitting} id="create-link-confirm"> | |||
{translate('create')} | |||
</SubmitButton> | |||
<ResetButtonLink disabled={submitting} onClick={onCloseClick}> | |||
{translate('cancel')} | |||
</ResetButtonLink> | |||
</footer> | |||
</form> | |||
)} | |||
</SimpleModal> | |||
); | |||
} | |||
} |
@@ -1,51 +0,0 @@ | |||
/* | |||
* 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 React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import CreationModal from './views/CreationModal'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default class Header extends React.PureComponent { | |||
static propTypes = { | |||
onCreate: PropTypes.func.isRequired | |||
}; | |||
handleCreateClick(e) { | |||
e.preventDefault(); | |||
e.target.blur(); | |||
new CreationModal({ | |||
onCreate: this.props.onCreate | |||
}).render(); | |||
} | |||
render() { | |||
return ( | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('project_links.page')}</h1> | |||
<div className="page-actions"> | |||
<button id="create-project-link" onClick={this.handleCreateClick.bind(this)}> | |||
{translate('create')} | |||
</button> | |||
</div> | |||
<div className="page-description">{translate('project_links.page.description')}</div> | |||
</header> | |||
); | |||
} | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* 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 CreationModal from './CreationModal'; | |||
import { Button } from '../../../components/ui/buttons'; | |||
import { translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
onCreate: (name: string, url: string) => Promise<void>; | |||
} | |||
interface State { | |||
creationModal: boolean; | |||
} | |||
export default class Header extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { creationModal: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleCreateClick = () => { | |||
this.setState({ creationModal: true }); | |||
}; | |||
handleCreationModalClose = () => { | |||
if (this.mounted) { | |||
this.setState({ creationModal: false }); | |||
} | |||
}; | |||
render() { | |||
return ( | |||
<> | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('project_links.page')}</h1> | |||
<div className="page-actions"> | |||
<Button id="create-project-link" onClick={this.handleCreateClick}> | |||
{translate('create')} | |||
</Button> | |||
</div> | |||
<div className="page-description">{translate('project_links.page.description')}</div> | |||
</header> | |||
{this.state.creationModal && ( | |||
<CreationModal onClose={this.handleCreationModalClose} onSubmit={this.props.onCreate} /> | |||
)} | |||
</> | |||
); | |||
} | |||
} |
@@ -17,25 +17,21 @@ | |||
* 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 * as React from 'react'; | |||
import { isProvided, getLinkName } from './utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { ProjectLink } from '../../../app/types'; | |||
import ConfirmButton from '../../../components/controls/ConfirmButton'; | |||
import BugTrackerIcon from '../../../components/ui/BugTrackerIcon'; | |||
import { Button } from '../../../components/ui/buttons'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
export default class LinkRow extends React.PureComponent { | |||
static propTypes = { | |||
link: PropTypes.object.isRequired, | |||
onDelete: PropTypes.func.isRequired | |||
}; | |||
handleDeleteClick(e) { | |||
e.preventDefault(); | |||
e.target.blur(); | |||
this.props.onDelete(); | |||
} | |||
interface Props { | |||
link: ProjectLink; | |||
onDelete: (linkId: string) => Promise<void>; | |||
} | |||
renderIcon(iconClassName) { | |||
export default class LinkRow extends React.PureComponent<Props> { | |||
renderIcon = (iconClassName: string) => { | |||
if (iconClassName === 'icon-issue') { | |||
return ( | |||
<div className="display-inline-block text-top spacer-right"> | |||
@@ -49,9 +45,9 @@ export default class LinkRow extends React.PureComponent { | |||
<i className={iconClassName} /> | |||
</div> | |||
); | |||
} | |||
}; | |||
renderNameForProvided(link) { | |||
renderNameForProvided = (link: ProjectLink) => { | |||
return ( | |||
<div> | |||
{this.renderIcon(`icon-${link.type}`)} | |||
@@ -65,9 +61,9 @@ export default class LinkRow extends React.PureComponent { | |||
</div> | |||
</div> | |||
); | |||
} | |||
}; | |||
renderName(link) { | |||
renderName = (link: ProjectLink) => { | |||
if (isProvided(link)) { | |||
return this.renderNameForProvided(link); | |||
} | |||
@@ -80,19 +76,32 @@ export default class LinkRow extends React.PureComponent { | |||
</div> | |||
</div> | |||
); | |||
} | |||
}; | |||
renderDeleteButton(link) { | |||
renderDeleteButton = (link: ProjectLink) => { | |||
if (isProvided(link)) { | |||
return null; | |||
} | |||
return ( | |||
<button className="button-red js-delete-button" onClick={this.handleDeleteClick.bind(this)}> | |||
{translate('delete')} | |||
</button> | |||
<ConfirmButton | |||
confirmButtonText={translate('delete')} | |||
confirmData={link.id} | |||
isDestructive={true} | |||
modalBody={translateWithParameters( | |||
'project_links.are_you_sure_to_delete_x_link', | |||
link.name | |||
)} | |||
modalHeader={translate('project_links.delete_project_link')} | |||
onConfirm={this.props.onDelete}> | |||
{({ onClick }) => ( | |||
<Button className="button-red js-delete-button" onClick={onClick}> | |||
{translate('delete')} | |||
</Button> | |||
)} | |||
</ConfirmButton> | |||
); | |||
} | |||
}; | |||
render() { | |||
const { link } = this.props; |
@@ -23,7 +23,6 @@ import Helmet from 'react-helmet'; | |||
import { connect } from 'react-redux'; | |||
import Header from './Header'; | |||
import Table from './Table'; | |||
import DeletionModal from './views/DeletionModal'; | |||
import { fetchProjectLinks, deleteProjectLink, createProjectLink } from '../store/actions'; | |||
import { getProjectAdminProjectLinks } from '../../../store/rootReducer'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -34,26 +33,17 @@ class Links extends React.PureComponent { | |||
links: PropTypes.array | |||
}; | |||
componentWillMount() { | |||
this.handleCreateLink = this.handleCreateLink.bind(this); | |||
this.handleDeleteLink = this.handleDeleteLink.bind(this); | |||
} | |||
componentDidMount() { | |||
this.props.fetchProjectLinks(this.props.component.key); | |||
} | |||
handleCreateLink(name, url) { | |||
handleCreateLink = (name, url) => { | |||
return this.props.createProjectLink(this.props.component.key, name, url); | |||
} | |||
}; | |||
handleDeleteLink(link) { | |||
new DeletionModal({ link }) | |||
.on('done', () => { | |||
this.props.deleteProjectLink(this.props.component.key, link.id); | |||
}) | |||
.render(); | |||
} | |||
handleDeleteLink = linkId => { | |||
return this.props.deleteProjectLink(this.props.component.key, linkId); | |||
}; | |||
render() { | |||
return ( |
@@ -29,10 +29,6 @@ export default class Table extends React.PureComponent { | |||
onDelete: PropTypes.func.isRequired | |||
}; | |||
handleDeleteLink(link) { | |||
this.props.onDelete(link); | |||
} | |||
renderHeader() { | |||
// keep empty cell for actions | |||
return ( | |||
@@ -50,12 +46,12 @@ export default class Table extends React.PureComponent { | |||
const orderedLinks = orderLinks(this.props.links); | |||
const linkRows = orderedLinks.map(link => ( | |||
<LinkRow key={link.id} link={link} onDelete={this.handleDeleteLink.bind(this, link)} /> | |||
<LinkRow key={link.id} link={link} onDelete={this.props.onDelete} /> | |||
)); | |||
return ( | |||
<div className="boxed-group boxed-group-inner"> | |||
<table id="project-links" className="data zebra"> | |||
<table className="data zebra" id="project-links"> | |||
{this.renderHeader()} | |||
<tbody>{linkRows}</tbody> | |||
</table> |
@@ -1,42 +0,0 @@ | |||
/* | |||
* 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 Template from './CreationModalTemplate.hbs'; | |||
import ModalForm from '../../../../components/common/modal-form'; | |||
import { parseError } from '../../../../helpers/request'; | |||
export default ModalForm.extend({ | |||
template: Template, | |||
onFormSubmit() { | |||
ModalForm.prototype.onFormSubmit.apply(this, arguments); | |||
this.disableForm(); | |||
const name = this.$('#create-link-name').val(); | |||
const url = this.$('#create-link-url').val(); | |||
this.options | |||
.onCreate(name, url) | |||
.then(() => this.destroy()) | |||
.catch(e => { | |||
parseError(e).then(msg => this.showSingleError(msg)); | |||
this.enableForm(); | |||
}); | |||
} | |||
}); |
@@ -1,22 +0,0 @@ | |||
<form> | |||
<div class="modal-head"> | |||
<h2>{{t 'project_links.create_new_project_link'}}</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
<div class="modal-field"> | |||
<label for="create-link-name">{{t 'project_links.name'}}<em class="mandatory">*</em></label> | |||
<input id="create-link-name" name="name" type="text" maxlength="128" required> | |||
</div> | |||
<div class="modal-field"> | |||
<label for="create-link-url">{{t 'project_links.url'}}<em class="mandatory">*</em></label> | |||
<input id="create-link-url" name="url" type="text" maxlength="2048" required> | |||
</div> | |||
</div> | |||
<div class="modal-foot"> | |||
<button id="create-link-confirm">{{t 'create'}}</button> | |||
<a href="#" class="js-modal-close">{{t 'cancel'}}</a> | |||
</div> | |||
</form> |
@@ -1,46 +0,0 @@ | |||
/* | |||
* 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 Template from './DeletionModalTemplate.hbs'; | |||
import ModalForm from '../../../../components/common/modal-form'; | |||
import { deleteLink } from '../../../../api/projectLinks'; | |||
import { parseError } from '../../../../helpers/request'; | |||
export default ModalForm.extend({ | |||
template: Template, | |||
onFormSubmit() { | |||
ModalForm.prototype.onFormSubmit.apply(this, arguments); | |||
this.disableForm(); | |||
deleteLink(this.options.link.id) | |||
.then(() => { | |||
this.trigger('done'); | |||
this.destroy(); | |||
}) | |||
.catch(e => { | |||
parseError(e).then(msg => this.showSingleError(msg)); | |||
this.enableForm(); | |||
}); | |||
}, | |||
serializeData() { | |||
return { link: this.options.link }; | |||
} | |||
}); |
@@ -1,13 +0,0 @@ | |||
<form> | |||
<div class="modal-head"> | |||
<h2>{{t 'project_links.delete_project_link'}}</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
{{tp 'project_links.are_you_sure_to_delete_x_link' link.name}} | |||
</div> | |||
<div class="modal-foot"> | |||
<button id="delete-link-confirm" class="button-red">{{t 'delete'}}</button> | |||
<a href="#" class="js-modal-close">{{t 'cancel'}}</a> | |||
</div> | |||
</form> |
@@ -17,8 +17,9 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getProjectLinks, createLink } from '../../../api/projectLinks'; | |||
import { getProjectLinks, createLink, deleteLink } from '../../../api/projectLinks'; | |||
import { getTree, changeKey as changeKeyApi } from '../../../api/components'; | |||
import throwGlobalError from '../../../app/utils/throwGlobalError'; | |||
export const RECEIVE_PROJECT_LINKS = 'projectAdmin/RECEIVE_PROJECT_LINKS'; | |||
export const receiveProjectLinks = (projectKey, links) => ({ | |||
@@ -28,9 +29,12 @@ export const receiveProjectLinks = (projectKey, links) => ({ | |||
}); | |||
export const fetchProjectLinks = projectKey => dispatch => { | |||
getProjectLinks(projectKey).then(links => { | |||
dispatch(receiveProjectLinks(projectKey, links)); | |||
}); | |||
getProjectLinks(projectKey).then( | |||
links => { | |||
dispatch(receiveProjectLinks(projectKey, links)); | |||
}, | |||
() => {} | |||
); | |||
}; | |||
export const ADD_PROJECT_LINK = 'projectAdmin/ADD_PROJECT_LINK'; | |||
@@ -47,12 +51,19 @@ export const createProjectLink = (projectKey, name, url) => dispatch => { | |||
}; | |||
export const DELETE_PROJECT_LINK = 'projectAdmin/DELETE_PROJECT_LINK'; | |||
export const deleteProjectLink = (projectKey, linkId) => ({ | |||
export const deleteProjectLinkAction = (projectKey, linkId) => ({ | |||
type: DELETE_PROJECT_LINK, | |||
projectKey, | |||
linkId | |||
}); | |||
export function deleteProjectLink(projectKey, linkId) { | |||
return dispatch => | |||
deleteLink(linkId).then(() => { | |||
dispatch(deleteProjectLinkAction(projectKey, linkId)); | |||
}); | |||
} | |||
export const RECEIVE_PROJECT_MODULES = 'projectAdmin/RECEIVE_PROJECT_MODULES'; | |||
const receiveProjectModules = (projectKey, modules) => ({ | |||
type: RECEIVE_PROJECT_MODULES, | |||
@@ -62,9 +73,12 @@ const receiveProjectModules = (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)); | |||
}); | |||
getTree(projectKey, options).then( | |||
r => { | |||
dispatch(receiveProjectModules(projectKey, r.components)); | |||
}, | |||
() => {} | |||
); | |||
}; | |||
export const CHANGE_KEY = 'projectAdmin/CHANGE_KEY'; | |||
@@ -75,5 +89,8 @@ const changeKeyAction = (key, newKey) => ({ | |||
}); | |||
export const changeKey = (key, newKey) => dispatch => { | |||
return changeKeyApi(key, newKey).then(() => dispatch(changeKeyAction(key, newKey))); | |||
return changeKeyApi(key, newKey).then( | |||
() => dispatch(changeKeyAction(key, newKey)), | |||
throwGlobalError | |||
); | |||
}; |
@@ -18,44 +18,55 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import React from 'react'; | |||
import ProjectsView from '../views/gate-projects-view'; | |||
import escapeHtml from 'escape-html'; | |||
import SelectList from '../../../components/SelectList'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
export default class Projects extends React.PureComponent { | |||
componentDidMount() { | |||
this.renderView(); | |||
this.renderSelectList(); | |||
} | |||
componentWillUpdate() { | |||
this.destroyView(); | |||
} | |||
componentDidUpdate() { | |||
this.renderView(); | |||
} | |||
renderSelectList = () => { | |||
if (!this.container) return; | |||
componentWillUnmount() { | |||
this.destroyView(); | |||
} | |||
const { qualityGate, edit, organization } = this.props; | |||
destroyView() { | |||
if (this.projectsView) { | |||
this.projectsView.destroy(); | |||
const extra = { gateId: qualityGate.id }; | |||
let orgQuery = ''; | |||
if (organization) { | |||
extra.organization = organization; | |||
orgQuery = '&organization=' + organization; | |||
} | |||
} | |||
renderView() { | |||
const { qualityGate, edit, organization } = this.props; | |||
this.projectsView = new ProjectsView({ | |||
qualityGate, | |||
edit, | |||
container: this.refs.container, | |||
organization | |||
// eslint-disable-next-line no-new | |||
new SelectList({ | |||
el: this.container, | |||
width: '100%', | |||
readOnly: !edit, | |||
focusSearch: false, | |||
dangerouslyUnescapedHtmlFormat: item => escapeHtml(item.name), | |||
searchUrl: getBaseUrl() + `/api/qualitygates/search?gateId=${qualityGate.id}${orgQuery}`, | |||
selectUrl: getBaseUrl() + '/api/qualitygates/select', | |||
deselectUrl: getBaseUrl() + '/api/qualitygates/deselect', | |||
extra, | |||
selectParameter: 'projectId', | |||
selectParameterValue: 'id', | |||
labels: { | |||
selected: translate('quality_gates.projects.with'), | |||
deselected: translate('quality_gates.projects.without'), | |||
all: translate('quality_gates.projects.all'), | |||
noResults: translate('quality_gates.projects.noResults') | |||
}, | |||
tooltips: { | |||
select: translate('quality_gates.projects.select_hint'), | |||
deselect: translate('quality_gates.projects.deselect_hint') | |||
} | |||
}); | |||
this.projectsView.render(); | |||
} | |||
}; | |||
render() { | |||
return <div ref="container" />; | |||
return <div ref={node => (this.container = node)} />; | |||
} | |||
} |
@@ -1,73 +0,0 @@ | |||
/* | |||
* 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 Marionette from 'backbone.marionette'; | |||
import escapeHtml from 'escape-html'; | |||
import SelectList from '../../../components/SelectList'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default Marionette.ItemView.extend({ | |||
template: () => {}, | |||
onRender() { | |||
const { qualityGate, organization } = this.options; | |||
const extra = { | |||
gateId: qualityGate.id | |||
}; | |||
let orgQuery = ''; | |||
if (organization) { | |||
extra.organization = organization; | |||
orgQuery = '&organization=' + organization; | |||
} | |||
new SelectList({ | |||
el: this.options.container, | |||
width: '100%', | |||
readOnly: !this.options.edit, | |||
focusSearch: false, | |||
dangerouslyUnescapedHtmlFormat(item) { | |||
return escapeHtml(item.name); | |||
}, | |||
searchUrl: `${window.baseUrl}/api/qualitygates/search?gateId=${qualityGate.id}${orgQuery}`, | |||
selectUrl: window.baseUrl + '/api/qualitygates/select', | |||
deselectUrl: window.baseUrl + '/api/qualitygates/deselect', | |||
extra, | |||
selectParameter: 'projectId', | |||
selectParameterValue: 'id', | |||
labels: { | |||
selected: translate('quality_gates.projects.with'), | |||
deselected: translate('quality_gates.projects.without'), | |||
all: translate('quality_gates.projects.all'), | |||
noResults: translate('quality_gates.projects.noResults') | |||
}, | |||
tooltips: { | |||
select: translate('quality_gates.projects.select_hint'), | |||
deselect: translate('quality_gates.projects.deselect_hint') | |||
} | |||
}); | |||
}, | |||
serializeData() { | |||
return { | |||
...Marionette.ItemView.prototype.serializeData.apply(this, arguments), | |||
canEdit: this.options.edit | |||
}; | |||
} | |||
}); |
@@ -1,47 +0,0 @@ | |||
/* | |||
* 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 Template from './templates/template.hbs'; | |||
import RestartingTemplate from './templates/restarting.hbs'; | |||
import ModalForm from '../common/modal-form'; | |||
import { restartAndWait } from '../../api/system'; | |||
const RestartModal = ModalForm.extend({ | |||
template: Template, | |||
restartingTemplate: RestartingTemplate, | |||
initialize() { | |||
this.restarting = false; | |||
}, | |||
getTemplate() { | |||
return this.restarting ? this.restartingTemplate : this.template; | |||
}, | |||
onFormSubmit() { | |||
ModalForm.prototype.onFormSubmit.apply(this, arguments); | |||
this.restarting = true; | |||
this.render(); | |||
restartAndWait().then(() => { | |||
document.location.reload(); | |||
}); | |||
} | |||
}); | |||
export default RestartModal; |
@@ -1,14 +0,0 @@ | |||
<form id="restart-server-form"> | |||
<div class="modal-head"> | |||
<h2>Restart Server</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
<p class="spacer-top spacer-bottom text-center"> | |||
Server is restarting. This page will be automatically refreshed. | |||
</p> | |||
<p class="big-spacer-top spacer-bottom text-center"> | |||
<i class="spinner"></i> | |||
</p> | |||
</div> | |||
</form> |
@@ -1,15 +0,0 @@ | |||
<form id="restart-server-form"> | |||
<div class="modal-head"> | |||
<h2>Restart Server</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
<p class="spacer-top spacer-bottom"> | |||
Are you sure you want to restart the server? | |||
</p> | |||
</div> | |||
<div class="modal-foot"> | |||
<button id="restart-server-submit">Restart</button> | |||
<a href="#" class="js-modal-close" id="restart-server-cancel">Cancel</a> | |||
</div> | |||
</form> |
@@ -1,99 +0,0 @@ | |||
/* | |||
* 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 ModalView from './modals'; | |||
export default ModalView.extend({ | |||
ui() { | |||
return { | |||
messagesContainer: '.js-modal-messages' | |||
}; | |||
}, | |||
events() { | |||
return { | |||
...ModalView.prototype.events.apply(this, arguments), | |||
'keydown input,textarea,select': 'onInputKeydown', | |||
'submit form': 'onFormSubmit' | |||
}; | |||
}, | |||
onRender() { | |||
ModalView.prototype.onRender.apply(this, arguments); | |||
const that = this; | |||
setTimeout(() => { | |||
that | |||
.$(':tabbable') | |||
.first() | |||
.focus(); | |||
}, 0); | |||
}, | |||
onInputKeydown(e) { | |||
if (e.keyCode === 27) { | |||
// escape | |||
this.destroy(); | |||
} | |||
}, | |||
onFormSubmit(e) { | |||
e.preventDefault(); | |||
}, | |||
showErrors(errors, warnings) { | |||
const container = this.ui.messagesContainer.empty(); | |||
if (Array.isArray(errors)) { | |||
errors.forEach(error => { | |||
const html = `<div class="alert alert-danger">${error.msg}</div>`; | |||
container.append(html); | |||
}); | |||
} | |||
if (Array.isArray(warnings)) { | |||
warnings.forEach(warn => { | |||
const html = `<div class="alert alert-warning">${warn.msg}</div>`; | |||
container.append(html); | |||
}); | |||
} | |||
this.ui.messagesContainer.scrollParent().scrollTop(0); | |||
}, | |||
showSingleError(msg) { | |||
this.showErrors([{ msg }], []); | |||
}, | |||
disableForm() { | |||
const form = this.$('form'); | |||
this.disabledFields = form.find(':input:not(:disabled)'); | |||
this.disabledFields.prop('disabled', true); | |||
}, | |||
enableForm() { | |||
if (this.disabledFields != null) { | |||
this.disabledFields.prop('disabled', false); | |||
} | |||
}, | |||
showSpinner() { | |||
this.$('.js-modal-spinner').removeClass('hidden'); | |||
}, | |||
hideSpinner() { | |||
this.$('.js-modal-spinner').addClass('hidden'); | |||
} | |||
}); |
@@ -1,92 +0,0 @@ | |||
/* | |||
* 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 $ from 'jquery'; | |||
import Marionette from 'backbone.marionette'; | |||
import key from 'keymaster'; | |||
const EVENT_SCOPE = 'modal'; | |||
export default Marionette.ItemView.extend({ | |||
className: 'modal', | |||
overlayClassName: 'modal-overlay', | |||
htmlClassName: 'modal-open', | |||
events() { | |||
return { | |||
'click .js-modal-close': 'onCloseClick' | |||
}; | |||
}, | |||
onRender() { | |||
const that = this; | |||
this.$el.detach().appendTo($('body')); | |||
$('html').addClass(this.htmlClassName); | |||
this.renderOverlay(); | |||
this.keyScope = key.getScope(); | |||
key.setScope('modal'); | |||
key('escape', 'modal', () => { | |||
that.destroy(); | |||
return false; | |||
}); | |||
this.show(); | |||
if (this.options.large) { | |||
this.$el.addClass('modal-large'); | |||
} | |||
}, | |||
show() { | |||
const that = this; | |||
setTimeout(() => { | |||
that.$el.addClass('in'); | |||
$('.' + that.overlayClassName).addClass('in'); | |||
}, 0); | |||
}, | |||
onDestroy() { | |||
$('html').removeClass(this.htmlClassName); | |||
this.removeOverlay(); | |||
key.deleteScope('modal'); | |||
key.setScope(this.keyScope); | |||
}, | |||
onCloseClick(e) { | |||
e.preventDefault(); | |||
this.destroy(); | |||
}, | |||
renderOverlay() { | |||
const overlay = $('.' + this.overlayClassName); | |||
if (overlay.length === 0) { | |||
$(`<div class="${this.overlayClassName}"></div>`).appendTo($('body')); | |||
} | |||
}, | |||
removeOverlay() { | |||
$('.' + this.overlayClassName).remove(); | |||
}, | |||
attachCloseEvents() { | |||
const that = this; | |||
$('body').on('click.' + EVENT_SCOPE, () => { | |||
$('body').off('click.' + EVENT_SCOPE); | |||
that.destroy(); | |||
}); | |||
} | |||
}); |
@@ -1,82 +0,0 @@ | |||
/* | |||
* 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 Marionette from 'backbone.marionette'; | |||
export default Marionette.CollectionView.extend({ | |||
initialize() { | |||
this.resetSelectedIndex(); | |||
this.listenTo(this.collection, 'reset', this.resetSelectedIndex); | |||
}, | |||
childViewOptions(model, index) { | |||
return { index }; | |||
}, | |||
resetSelectedIndex() { | |||
this.selectedIndex = 0; | |||
}, | |||
onRender() { | |||
this.selectCurrent(); | |||
}, | |||
submitCurrent() { | |||
const view = this.children.findByIndex(this.selectedIndex); | |||
if (view != null) { | |||
view.submit(); | |||
} | |||
}, | |||
selectCurrent() { | |||
this.selectItem(this.selectedIndex); | |||
}, | |||
selectNext() { | |||
if (this.selectedIndex < this.collection.length - 1) { | |||
this.deselectItem(this.selectedIndex); | |||
this.selectedIndex++; | |||
this.selectItem(this.selectedIndex); | |||
} | |||
}, | |||
selectPrev() { | |||
if (this.selectedIndex > 0) { | |||
this.deselectItem(this.selectedIndex); | |||
this.selectedIndex--; | |||
this.selectItem(this.selectedIndex); | |||
} | |||
}, | |||
selectItem(index) { | |||
if (index >= 0 && index < this.collection.length) { | |||
const view = this.children.findByIndex(index); | |||
if (view != null) { | |||
view.select(); | |||
} | |||
} | |||
}, | |||
deselectItem(index) { | |||
const view = this.children.findByIndex(index); | |||
if (view != null) { | |||
view.deselect(); | |||
} | |||
} | |||
}); |
@@ -23,8 +23,13 @@ import DeferredSpinner from '../common/DeferredSpinner'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { SubmitButton, ResetButtonLink } from '../ui/buttons'; | |||
export interface ChildrenProps { | |||
onClick: () => void; | |||
onFormSubmit: (event: React.FormEvent<HTMLFormElement>) => void; | |||
} | |||
interface Props { | |||
children: (props: { onClick: () => void }) => React.ReactNode; | |||
children: (props: ChildrenProps) => React.ReactNode; | |||
confirmButtonText: string; | |||
confirmData?: string; | |||
isDestructive?: boolean; | |||
@@ -53,6 +58,13 @@ export default class ConfirmButton extends React.PureComponent<Props, State> { | |||
this.setState({ modal: true }); | |||
}; | |||
handleFormSubmit = (event?: React.FormEvent<HTMLFormElement>) => { | |||
if (event) { | |||
event.preventDefault(); | |||
} | |||
this.setState({ modal: true }); | |||
}; | |||
handleSubmit = () => { | |||
const result = this.props.onConfirm(this.props.confirmData); | |||
if (result) { | |||
@@ -74,7 +86,10 @@ export default class ConfirmButton extends React.PureComponent<Props, State> { | |||
return ( | |||
<> | |||
{this.props.children({ onClick: this.handleButtonClick })} | |||
{this.props.children({ | |||
onClick: this.handleButtonClick, | |||
onFormSubmit: this.handleFormSubmit | |||
})} | |||
{this.state.modal && ( | |||
<SimpleModal | |||
header={modalHeader} |
@@ -100,7 +100,7 @@ public class ProjectKeyUpdatePageTest { | |||
ProjectKeyPage page = openPage("sample"); | |||
page.openFineGrainedUpdate().tryFineGrainedUpdate("sample:module_a:module_a1", "another"); | |||
$("#update-key-confirmation-form").shouldNotBe(visible); | |||
$(".modal").shouldNotBe(visible); | |||
tester.openBrowser().openProjectKey("another"); | |||
assertThat(url()).endsWith("/project/key?id=another"); |
@@ -37,6 +37,7 @@ import org.sonarqube.ws.client.projectlinks.CreateRequest; | |||
import org.sonarqube.ws.client.projectlinks.DeleteRequest; | |||
import static com.codeborne.selenide.Condition.text; | |||
import static com.codeborne.selenide.Condition.visible; | |||
import static com.codeborne.selenide.Selenide.$; | |||
import static util.ItUtils.projectDir; | |||
@@ -88,7 +89,7 @@ public class ProjectLinksTest { | |||
customLink.getName().should(text("Custom")); | |||
customLink.getType().shouldNot(Condition.exist); | |||
customLink.getUrl().should(text("http://example.org/custom")); | |||
customLink.getDeleteButton().shouldBe(Condition.visible); | |||
customLink.getDeleteButton().shouldBe(visible); | |||
} | |||
@Test | |||
@@ -109,7 +110,7 @@ public class ProjectLinksTest { | |||
testLink.getName().should(text("Test")); | |||
testLink.getType().shouldNot(Condition.exist); | |||
testLink.getUrl().should(text("http://example.com/test")); | |||
testLink.getDeleteButton().shouldBe(Condition.visible); | |||
testLink.getDeleteButton().shouldBe(visible); | |||
} | |||
@Test | |||
@@ -122,9 +123,8 @@ public class ProjectLinksTest { | |||
ProjectLinkItem customLink = links.get(1); | |||
customLink.getDeleteButton().click(); | |||
$("#delete-link-confirm") | |||
.shouldBe(Condition.visible) | |||
.click(); | |||
$(".modal").shouldBe(visible); | |||
$(".modal button[type=\"submit\"]").click(); | |||
page.getLinks().shouldHaveSize(1); | |||
} |