From 0b1226871a26f136739f29050de52088b2aa1c3e Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Fri, 29 Jul 2016 16:08:36 +0200 Subject: [PATCH] SONAR-7920 Rewrite Links project page (#1127) --- .../src/test/java/it/Category1Suite.java | 2 + .../ProjectLinksPageTest.java | 153 ++++++++++++++++++ .../src/test/java/pageobjects/LoginPage.java | 6 +- .../src/test/java/pageobjects/Navigation.java | 6 + .../java/pageobjects/ProjectLinkItem.java | 52 ++++++ .../java/pageobjects/ProjectLinksPage.java | 47 ++++++ .../sonar-web/src/main/js/api/projectLinks.js | 38 +++++ .../src/main/js/apps/project-admin/app.js | 4 + .../js/apps/project-admin/links/Header.js | 56 +++++++ .../js/apps/project-admin/links/LinkRow.js | 113 +++++++++++++ .../main/js/apps/project-admin/links/Links.js | 82 ++++++++++ .../main/js/apps/project-admin/links/Table.js | 74 +++++++++ .../main/js/apps/project-admin/links/utils.js | 46 ++++++ .../links/views/CreationModal.js | 45 ++++++ .../links/views/CreationModalTemplate.hbs | 22 +++ .../links/views/DeletionModal.js | 48 ++++++ .../links/views/DeletionModalTemplate.hbs | 13 ++ .../js/apps/project-admin/store/actions.js | 34 ++++ .../main/js/apps/project-admin/store/links.js | 45 ++++++ .../project-admin/store/linksByProject.js | 49 ++++++ .../apps/project-admin/store/rootReducer.js | 13 +- .../app/controllers/project_controller.rb | 46 +----- .../WEB-INF/app/views/project/links.html.erb | 138 +--------------- .../resources/org/sonar/l10n/core.properties | 6 + 24 files changed, 963 insertions(+), 175 deletions(-) create mode 100644 it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java create mode 100644 it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java create mode 100644 it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java create mode 100644 server/sonar-web/src/main/js/api/projectLinks.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/Header.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/Links.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/Table.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/utils.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs create mode 100644 server/sonar-web/src/main/js/apps/project-admin/store/links.js create mode 100644 server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js diff --git a/it/it-tests/src/test/java/it/Category1Suite.java b/it/it-tests/src/test/java/it/Category1Suite.java index 65ff384d431..b613cc3ec2b 100644 --- a/it/it-tests/src/test/java/it/Category1Suite.java +++ b/it/it-tests/src/test/java/it/Category1Suite.java @@ -43,6 +43,7 @@ import it.measureHistory.TimeMachineTest; import it.projectAdministration.BackgroundTasksTest; import it.projectAdministration.BulkDeletionTest; import it.projectAdministration.ProjectAdministrationTest; +import it.projectAdministration.ProjectLinksPageTest; import it.qualityGate.QualityGateNotificationTest; import it.qualityGate.QualityGateTest; import it.qualityGate.QualityGateUiTest; @@ -66,6 +67,7 @@ import static util.ItUtils.xooPlugin; // project administration BulkDeletionTest.class, ProjectAdministrationTest.class, + ProjectLinksPageTest.class, BackgroundTasksTest.class, // settings PropertySetsTest.class, diff --git a/it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java b/it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java new file mode 100644 index 00000000000..6faea2774f0 --- /dev/null +++ b/it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java @@ -0,0 +1,153 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package it.projectAdministration; + +import com.codeborne.selenide.Condition; +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.SonarScanner; +import it.Category1Suite; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.sonarqube.ws.WsProjectLinks.CreateWsResponse; +import org.sonarqube.ws.client.WsClient; +import org.sonarqube.ws.client.projectlinks.CreateWsRequest; +import org.sonarqube.ws.client.projectlinks.DeleteWsRequest; +import pageobjects.Navigation; +import pageobjects.ProjectLinkItem; +import pageobjects.ProjectLinksPage; + +import static com.codeborne.selenide.Condition.hasText; +import static com.codeborne.selenide.Selenide.$; +import static util.ItUtils.newAdminWsClient; +import static util.ItUtils.projectDir; + +public class ProjectLinksPageTest { + + @ClassRule + public static Orchestrator ORCHESTRATOR = Category1Suite.ORCHESTRATOR; + + @Rule + public Navigation nav = Navigation.get(ORCHESTRATOR); + + private static WsClient wsClient; + private long customLinkId; + + @BeforeClass + public static void setUp() { + wsClient = newAdminWsClient(ORCHESTRATOR); + + ORCHESTRATOR.resetData(); + ORCHESTRATOR.executeBuild( + SonarScanner.create(projectDir("shared/xoo-sample")) + .setProperty("sonar.links.homepage", "http://example.com")); + } + + @Before + public void prepare() { + customLinkId = Long.parseLong(createCustomLink().getLink().getId()); + } + + @After + public void clean() { + deleteLink(customLinkId); + } + + @Test + public void should_list_links() { + ProjectLinksPage page = openPage(); + + page.getLinks().shouldHaveSize(2); + + List links = page.getLinksAsItems(); + ProjectLinkItem homepageLink = links.get(0); + ProjectLinkItem customLink = links.get(1); + + homepageLink.getName().should(hasText("Home")); + homepageLink.getType().should(hasText("sonar.links.homepage")); + homepageLink.getUrl().should(hasText("http://example.com")); + homepageLink.getDeleteButton().shouldNot(Condition.present); + + customLink.getName().should(hasText("Custom")); + customLink.getType().shouldNot(Condition.present); + customLink.getUrl().should(hasText("http://example.org/custom")); + customLink.getDeleteButton().shouldBe(Condition.visible); + } + + @Test + public void should_create_link() { + ProjectLinksPage page = openPage(); + + page.getLinks().shouldHaveSize(2); + + $("#create-project-link").click(); + $("#create-link-name").setValue("Test"); + $("#create-link-url").setValue("http://example.com/test"); + $("#create-link-confirm").click(); + + page.getLinks().shouldHaveSize(3); + + ProjectLinkItem testLink = page.getLinksAsItems().get(2); + + testLink.getName().should(hasText("Test")); + testLink.getType().shouldNot(Condition.present); + testLink.getUrl().should(hasText("http://example.com/test")); + testLink.getDeleteButton().shouldBe(Condition.visible); + } + + @Test + public void should_delete_link() { + ProjectLinksPage page = openPage(); + + page.getLinks().shouldHaveSize(2); + + List links = page.getLinksAsItems(); + ProjectLinkItem customLink = links.get(1); + + customLink.getDeleteButton().click(); + $("#delete-link-confirm").click(); + + page.getLinks().shouldHaveSize(1); + } + + private CreateWsResponse createCustomLink() { + return wsClient.projectLinks().create(new CreateWsRequest() + .setProjectKey("sample") + .setName("Custom") + .setUrl("http://example.org/custom")); + } + + private void deleteLink(long id) { + try { + wsClient.projectLinks().delete(new DeleteWsRequest().setId(id)); + } catch (Exception e) { + // fail silently + } + } + + private ProjectLinksPage openPage() { + nav.logIn().submitCredentials("admin", "admin"); + return nav.openProjectLinks("sample"); + } +} diff --git a/it/it-tests/src/test/java/pageobjects/LoginPage.java b/it/it-tests/src/test/java/pageobjects/LoginPage.java index bf946b27afa..2aea164f0cb 100644 --- a/it/it-tests/src/test/java/pageobjects/LoginPage.java +++ b/it/it-tests/src/test/java/pageobjects/LoginPage.java @@ -37,7 +37,10 @@ public class LoginPage { } public LoginPage submitWrongCredentials(String login, String password) { - return submitCredentials(login, password, LoginPage.class); + $("#login").val(login); + $("#password").val(password); + $(By.name("commit")).click(); + return page(LoginPage.class); } public SelenideElement getErrorMessage() { @@ -48,6 +51,7 @@ public class LoginPage { $("#login").val(login); $("#password").val(password); $(By.name("commit")).click(); + $("#login").should(Condition.disappear); return page(expectedResultPage); } } diff --git a/it/it-tests/src/test/java/pageobjects/Navigation.java b/it/it-tests/src/test/java/pageobjects/Navigation.java index 395509c5fad..46ee57ca425 100644 --- a/it/it-tests/src/test/java/pageobjects/Navigation.java +++ b/it/it-tests/src/test/java/pageobjects/Navigation.java @@ -50,6 +50,12 @@ public class Navigation extends ExternalResource { return open("/", Navigation.class); } + public ProjectLinksPage openProjectLinks(String projectKey) { + // TODO encode projectKey + String url = "/project/links?id=" + projectKey; + return open(url, ProjectLinksPage.class); + } + public void open(String relativeUrl) { Selenide.open(relativeUrl); } diff --git a/it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java b/it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java new file mode 100644 index 00000000000..cf5462009cd --- /dev/null +++ b/it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package pageobjects; + +import com.codeborne.selenide.SelenideElement; +import org.openqa.selenium.NoSuchElementException; + +public class ProjectLinkItem { + + private final SelenideElement elt; + + public ProjectLinkItem(SelenideElement elt) { + this.elt = elt; + } + + public SelenideElement getName() { + return elt.$(".js-name"); + } + + public SelenideElement getType() { + try { + return elt.$(".js-type"); + } catch (NoSuchElementException e) { + return null; + } + } + + public SelenideElement getUrl() { + return elt.$(".js-url"); + } + + public SelenideElement getDeleteButton() { + return elt.$(".js-delete-button"); + } +} diff --git a/it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java b/it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java new file mode 100644 index 00000000000..fb143e560b6 --- /dev/null +++ b/it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package pageobjects; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.ElementsCollection; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.codeborne.selenide.Selenide.$; +import static com.codeborne.selenide.Selenide.$$; + +public class ProjectLinksPage { + + public ProjectLinksPage() { + $("#project-links").should(Condition.exist); + } + + public ElementsCollection getLinks() { + return $$("#project-links tr[data-name]"); + } + + public List getLinksAsItems() { + return getLinks() + .stream() + .map(ProjectLinkItem::new) + .collect(Collectors.toList()); + } +} diff --git a/server/sonar-web/src/main/js/api/projectLinks.js b/server/sonar-web/src/main/js/api/projectLinks.js new file mode 100644 index 00000000000..06e1eaa7d26 --- /dev/null +++ b/server/sonar-web/src/main/js/api/projectLinks.js @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { getJSON, post, postJSON } from '../helpers/request'; + +export function getProjectLinks (projectKey) { + const url = '/api/project_links/search'; + const data = { projectKey }; + return getJSON(url, data).then(r => r.links); +} + +export function deleteLink (linkId) { + const url = '/api/project_links/delete'; + const data = { id: linkId }; + return post(url, data); +} + +export function createLink (projectKey, name, url) { + const apiURL = '/api/project_links/create'; + const data = { projectKey, name, url }; + return postJSON(apiURL, data).then(r => r.link); +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/app.js b/server/sonar-web/src/main/js/apps/project-admin/app.js index 7bcb91f9e1c..85fb770962f 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/app.js +++ b/server/sonar-web/src/main/js/apps/project-admin/app.js @@ -24,6 +24,7 @@ import { Router, Route, useRouterHistory } from 'react-router'; import { createHistory } from 'history'; import Deletion from './deletion/Deletion'; import QualityProfiles from './quality-profiles/QualityProfiles'; +import Links from './links/Links'; import rootReducer from './store/rootReducer'; import configureStore from '../../components/store/configureStore'; @@ -48,6 +49,9 @@ window.sonarqube.appStarted.then(options => { + ), el); diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/Header.js b/server/sonar-web/src/main/js/apps/project-admin/links/Header.js new file mode 100644 index 00000000000..e6471ca9fb6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/Header.js @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import CreationModal from './views/CreationModal'; +import { translate } from '../../../helpers/l10n'; + +export default class Header extends React.Component { + static propTypes = { + onCreate: React.PropTypes.func.isRequired + }; + + handleCreateClick (e) { + e.preventDefault(); + e.target.blur(); + new CreationModal({ + onCreate: this.props.onCreate + }).render(); + } + + render () { + return ( +
+

+ {translate('project_links.page')} +

+
+ +
+
+ {translate('project_links.page.description')} +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js b/server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js new file mode 100644 index 00000000000..e55651dce89 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js @@ -0,0 +1,113 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { isProvided, isClickable } from './utils'; +import { translate } from '../../../helpers/l10n'; + +export default class LinkRow extends React.Component { + static propTypes = { + link: React.PropTypes.object.isRequired, + onDelete: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleDeleteClick (e) { + e.preventDefault(); + e.target.blur(); + this.props.onDelete(); + } + + renderIcon (iconClassName) { + return ( +
+ +
+ ); + } + + renderNameForProvided (link) { + return ( +
+ {this.renderIcon(`icon-${link.type}`)} +
+
+ {link.name} +
+
+ {`sonar.links.${link.type}`} +
+
+
+ ); + } + + renderName (link) { + if (isProvided(link)) { + return this.renderNameForProvided(link); + } + + return ( +
+ {this.renderIcon('icon-detach')} +
+ {link.name} +
+
+ ); + } + + renderUrl (link) { + if (isClickable(link)) { + return {link.url}; + } + + return link.url; + } + + renderDeleteButton (link) { + if (isProvided(link)) { + return null; + } + + return ( + + ); + } + + render () { + const { link } = this.props; + + return ( + + {this.renderName(link)} + {this.renderUrl(link)} + {this.renderDeleteButton(link)} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/Links.js b/server/sonar-web/src/main/js/apps/project-admin/links/Links.js new file mode 100644 index 00000000000..88602532ea9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/Links.js @@ -0,0 +1,82 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import { connect } from 'react-redux'; +import Header from './Header'; +import Table from './Table'; +import DeletionModal from './views/DeletionModal'; +import { getProjectLinks } from '../store/rootReducer'; +import { + fetchProjectLinks, + deleteProjectLink, + createProjectLink +} from '../store/actions'; + +class Links extends React.Component { + static propTypes = { + component: React.PropTypes.object.isRequired, + links: React.PropTypes.array + }; + + componentWillMount () { + this.handleCreateLink = this.handleCreateLink.bind(this); + this.handleDeleteLink = this.handleDeleteLink.bind(this); + } + + componentDidMount () { + this.props.fetchProjectLinks(this.props.component.key); + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + 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(); + } + + render () { + return ( +
+
+ + + ); + } +} + +const mapStateToProps = (state, ownProps) => ({ + links: getProjectLinks(state, ownProps.component.key) +}); + +export default connect( + mapStateToProps, + { fetchProjectLinks, createProjectLink, deleteProjectLink } +)(Links); diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/Table.js b/server/sonar-web/src/main/js/apps/project-admin/links/Table.js new file mode 100644 index 00000000000..95f37f6841c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/Table.js @@ -0,0 +1,74 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import React from 'react'; +import shallowCompare from 'react-addons-shallow-compare'; +import LinkRow from './LinkRow'; +import { orderLinks } from './utils'; +import { translate } from '../../../helpers/l10n'; + +export default class Table extends React.Component { + static propTypes = { + links: React.PropTypes.array.isRequired, + onDelete: React.PropTypes.func.isRequired + }; + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + handleDeleteLink (link) { + this.props.onDelete(link); + } + + renderHeader () { + // keep empty cell for actions + return ( + + + + + + + + ); + } + + render () { + const orderedLinks = orderLinks(this.props.links); + + const linkRows = orderedLinks.map(link => ( + + )); + + return ( +
+ {translate('project_links.name')} + + {translate('project_links.url')} +  
+ {this.renderHeader()} + {linkRows} + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/utils.js b/server/sonar-web/src/main/js/apps/project-admin/links/utils.js new file mode 100644 index 00000000000..9a34509d38b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/utils.js @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import partition from 'lodash/partition'; +import sortBy from 'lodash/sortBy'; + +const PROVIDED_TYPES = [ + 'homepage', + 'ci', + 'issue', + 'scm', + 'scm_dev' +]; + +export function isProvided (link) { + return PROVIDED_TYPES.includes(link.type); +} + +export function orderLinks (links) { + const [provided, unknown] = partition(links, isProvided); + return [ + ...sortBy(provided, link => PROVIDED_TYPES.indexOf(link.type)), + ...sortBy(unknown, link => link.name) + ]; +} + +export function isClickable (link) { + // stupid simple check + return link.url.indexOf('http') === 0; +} diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js b/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js new file mode 100644 index 00000000000..a2fa0e36358 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import ModalForm from '../../../../components/common/modal-form'; +import Template from './CreationModalTemplate.hbs'; + +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(function (e) { + e.response.json().then(r => { + this.showErrors(r.errors, r.warnings); + this.enableForm(); + }); + }); + } +}); + diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs b/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs new file mode 100644 index 00000000000..7405f30d1b4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs @@ -0,0 +1,22 @@ +
+ + + +
diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js b/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js new file mode 100644 index 00000000000..a1064dfb66e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import ModalForm from '../../../../components/common/modal-form'; +import Template from './DeletionModalTemplate.hbs'; +import { deleteLink } from '../../../../api/projectLinks'; + +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(function (e) { + e.response.json().then(r => { + this.showErrors(r.errors, r.warnings); + this.enableForm(); + }); + }); + }, + + serializeData () { + return { link: this.options.link }; + } +}); + diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs b/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs new file mode 100644 index 00000000000..b8d744c83d9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs @@ -0,0 +1,13 @@ +
+ + + +
diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js index cad25a70e7a..083a28bfa92 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/actions.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/actions.js @@ -23,6 +23,7 @@ import { dissociateProject } from '../../../api/quality-profiles'; import { getProfileByKey } from './rootReducer'; +import { getProjectLinks, createLink } from '../../../api/projectLinks'; export const RECEIVE_PROFILES = 'RECEIVE_PROFILES'; export const receiveProfiles = profiles => ({ @@ -68,3 +69,36 @@ export const setProjectProfile = (projectKey, oldKey, newKey) => dispatch(setProjectProfileAction(projectKey, oldKey, newKey)); }); }; + +export const RECEIVE_PROJECT_LINKS = 'RECEIVE_PROJECT_LINKS'; +export const receiveProjectLinks = (projectKey, links) => ({ + type: RECEIVE_PROJECT_LINKS, + projectKey, + links +}); + +export const fetchProjectLinks = projectKey => dispatch => { + getProjectLinks(projectKey).then(links => { + dispatch(receiveProjectLinks(projectKey, links)); + }); +}; + +export const ADD_PROJECT_LINK = 'ADD_PROJECT_LINK'; +const addProjectLink = (projectKey, link) => ({ + type: ADD_PROJECT_LINK, + projectKey, + link +}); + +export const createProjectLink = (projectKey, name, url) => dispatch => { + return createLink(projectKey, name, url).then(link => { + dispatch(addProjectLink(projectKey, link)); + }); +}; + +export const DELETE_PROJECT_LINK = 'DELETE_PROJECT_LINK'; +export const deleteProjectLink = (projectKey, linkId) => ({ + type: DELETE_PROJECT_LINK, + projectKey, + linkId +}); diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/links.js b/server/sonar-web/src/main/js/apps/project-admin/store/links.js new file mode 100644 index 00000000000..9a79d3d7905 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/links.js @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import keyBy from 'lodash/keyBy'; +import omit from 'lodash/omit'; +import { RECEIVE_PROJECT_LINKS, DELETE_PROJECT_LINK } from './actions'; +import { ADD_PROJECT_LINK } from './actions'; + +const links = (state = {}, action = {}) => { + if (action.type === RECEIVE_PROJECT_LINKS) { + const newLinksById = keyBy(action.links, 'id'); + return { ...state, ...newLinksById }; + } + + if (action.type === ADD_PROJECT_LINK) { + return { ...state, [action.link.id]: action.link }; + } + + if (action.type === DELETE_PROJECT_LINK) { + return omit(state, action.linkId); + } + + return state; +}; + +export default links; + +export const getLink = (state, id) => + state[id]; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js b/server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js new file mode 100644 index 00000000000..dd48ff45f98 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2016 SonarSource SA + * mailto:contact AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import without from 'lodash/without'; +import { + RECEIVE_PROJECT_LINKS, + DELETE_PROJECT_LINK, + ADD_PROJECT_LINK +} from './actions'; + +const linksByProject = (state = {}, action = {}) => { + if (action.type === RECEIVE_PROJECT_LINKS) { + const linkIds = action.links.map(link => link.id); + return { ...state, [action.projectKey]: linkIds }; + } + + if (action.type === ADD_PROJECT_LINK) { + const byProject = state[action.projectKey] || []; + const ids = [...byProject, action.link.id]; + return { ...state, [action.projectKey]: ids }; + } + + if (action.type === DELETE_PROJECT_LINK) { + const ids = without(state[action.projectKey], action.linkId); + return { ...state, [action.projectKey]: ids }; + } + + return state; +}; + +export default linksByProject; + +export const getLinks = (state, projectKey) => state[projectKey] || []; diff --git a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js index 5f5dc3d7899..e8174f53aa0 100644 --- a/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js +++ b/server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js @@ -23,10 +23,14 @@ import profiles, { getAllProfiles as nextGetAllProfiles } from './profiles'; import profilesByProject, { getProfiles } from './profilesByProject'; +import links, { getLink } from './links'; +import linksByProject, { getLinks } from './linksByProject'; const rootReducer = combineReducers({ profiles, - profilesByProject + profilesByProject, + links, + linksByProject }); export default rootReducer; @@ -40,3 +44,10 @@ export const getAllProfiles = state => export const getProjectProfiles = (state, projectKey) => getProfiles(state.profilesByProject, projectKey) .map(profileKey => getProfileByKey(state, profileKey)); + +export const getLinkById = (state, linkId) => + getLink(state.links, linkId); + +export const getProjectLinks = (state, projectKey) => + getLinks(state.linksByProject, projectKey) + .map(linkId => getLinkById(state, linkId)); diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb index 5754eccd086..dbd5d6a434e 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb @@ -44,6 +44,14 @@ class ProjectController < ApplicationController redirect_to(url_for({:action => 'quality_profiles'}) + '?id=' + url_encode(params[:id])) end + def background_tasks + @project = get_current_project(params[:id]) + end + + def links + @project = get_current_project(params[:id]) + end + # GET /project/qualitygate?id= def qualitygate require_parameters :id @@ -154,44 +162,6 @@ class ProjectController < ApplicationController :include => 'events', :order => 'snapshots.created_at DESC') end - def background_tasks - @project = get_current_project(params[:id]) - end - - def links - @project = get_current_project(params[:id]) - - if !@project.project? - redirect_to :action => 'index', :id => params[:id] - end - @snapshot = @project.last_snapshot - end - - def set_links - project = get_current_project(params[:project_id]) - - project.custom_links.each { |link| link.delete } - - params.each_pair do |param_key, value| - if (param_key.starts_with?('name_')) - id = param_key[5..-1] - name=value - url=params["url_#{id}"] - key=params["key_#{id}"] - if key.blank? - key=ProjectLink.name_to_key(name) - end - unless key.blank? || name.blank? || url.blank? - project.links.create(:href => url, :name => name, :link_type => key) - end - end - end - project.save! - - flash[:notice] = 'Links updated.' - redirect_to :action => 'links', :id => project.id - end - def settings @resource = get_current_project(params[:id]) diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb index 0c64f689e8f..e9dd9ae3410 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb @@ -1,135 +1,3 @@ -
- - - - - -
+<% content_for :extra_script do %> + +<% end %> 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 0a86b12052e..7878ece8129 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -419,6 +419,12 @@ project_links.scm=Sources project_links.scm_ro=Read-only connection project_links.scm_dev=Developer connection +project_links.create_new_project_link=Create New Project Link +project_links.delete_project_link=Delete Project Link +project_links.are_you_sure_to_delete_x_link=Are you sure you want to delete the "{0}" link? +project_links.name=Name +project_links.url=URL + #------------------------------------------------------------------------------ # -- 2.39.5