aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-07-29 16:08:36 +0200
committerGitHub <noreply@github.com>2016-07-29 16:08:36 +0200
commit0b1226871a26f136739f29050de52088b2aa1c3e (patch)
tree1124abd73a6cf58d45e17cc0fb075a4076ca4320
parent3e3e4b1e16c0e5ebf8a485d3014cb7bd2577e192 (diff)
downloadsonarqube-0b1226871a26f136739f29050de52088b2aa1c3e.tar.gz
sonarqube-0b1226871a26f136739f29050de52088b2aa1c3e.zip
SONAR-7920 Rewrite Links project page (#1127)
-rw-r--r--it/it-tests/src/test/java/it/Category1Suite.java2
-rw-r--r--it/it-tests/src/test/java/it/projectAdministration/ProjectLinksPageTest.java153
-rw-r--r--it/it-tests/src/test/java/pageobjects/LoginPage.java6
-rw-r--r--it/it-tests/src/test/java/pageobjects/Navigation.java6
-rw-r--r--it/it-tests/src/test/java/pageobjects/ProjectLinkItem.java52
-rw-r--r--it/it-tests/src/test/java/pageobjects/ProjectLinksPage.java47
-rw-r--r--server/sonar-web/src/main/js/api/projectLinks.js38
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/app.js4
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/Header.js56
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js113
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/Links.js82
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/Table.js74
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/utils.js46
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js45
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs22
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js48
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/actions.js34
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/links.js45
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/linksByProject.js49
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js13
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/controllers/project_controller.rb46
-rw-r--r--server/sonar-web/src/main/webapp/WEB-INF/app/views/project/links.html.erb138
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties6
24 files changed, 963 insertions, 175 deletions
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<ProjectLinkItem> 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<ProjectLinkItem> 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<ProjectLinkItem> 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 => {
<Route
path="/quality_profiles"
component={withComponent(QualityProfiles)}/>
+ <Route
+ path="/links"
+ component={withComponent(Links)}/>
</Router>
</Provider>
), 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 (
+ <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>
+ );
+ }
+}
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 (
+ <div className="display-inline-block text-top spacer-right">
+ <i className={iconClassName}/>
+ </div>
+ );
+ }
+
+ renderNameForProvided (link) {
+ return (
+ <div>
+ {this.renderIcon(`icon-${link.type}`)}
+ <div className="display-inline-block text-top">
+ <div>
+ <span className="js-name">{link.name}</span>
+ </div>
+ <div className="note little-spacer-top">
+ <span className="js-type">{`sonar.links.${link.type}`}</span>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ renderName (link) {
+ if (isProvided(link)) {
+ return this.renderNameForProvided(link);
+ }
+
+ return (
+ <div>
+ {this.renderIcon('icon-detach')}
+ <div className="display-inline-block text-top">
+ <span className="js-name">{link.name}</span>
+ </div>
+ </div>
+ );
+ }
+
+ renderUrl (link) {
+ if (isClickable(link)) {
+ return <a href={link.url} target="_blank">{link.url}</a>;
+ }
+
+ return link.url;
+ }
+
+ renderDeleteButton (link) {
+ if (isProvided(link)) {
+ return null;
+ }
+
+ return (
+ <button
+ className="button-red js-delete-button"
+ onClick={this.handleDeleteClick.bind(this)}>
+ {translate('delete')}
+ </button>
+ );
+ }
+
+ render () {
+ const { link } = this.props;
+
+ return (
+ <tr data-name={link.name}>
+ <td className="nowrap">{this.renderName(link)}</td>
+ <td className="nowrap js-url">{this.renderUrl(link)}</td>
+ <td className="thin nowrap">{this.renderDeleteButton(link)}</td>
+ </tr>
+ );
+ }
+}
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 (
+ <div className="page page-limited">
+ <Header
+ onCreate={this.handleCreateLink}/>
+ <Table
+ links={this.props.links}
+ onDelete={this.handleDeleteLink}/>
+ </div>
+ );
+ }
+}
+
+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 (
+ <thead>
+ <tr>
+ <th className="nowrap">
+ {translate('project_links.name')}
+ </th>
+ <th className="nowrap width-100">
+ {translate('project_links.url')}
+ </th>
+ <th className="thin">&nbsp;</th>
+ </tr>
+ </thead>
+ );
+ }
+
+ render () {
+ const orderedLinks = orderLinks(this.props.links);
+
+ const linkRows = orderedLinks.map(link => (
+ <LinkRow
+ key={link.id}
+ link={link}
+ onDelete={this.handleDeleteLink.bind(this, link)}/>
+ ));
+
+ return (
+ <table id="project-links" className="data zebra">
+ {this.renderHeader()}
+ <tbody>{linkRows}</tbody>
+ </table>
+ );
+ }
+}
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 @@
+<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" 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" 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>
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 @@
+<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>
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=<project 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 @@
-<div class="page">
- <header class="page-header">
- <h1 class="page-title"><%= message('project_links.page') -%></h1>
- <p class="page-description"><%= message('project_links.page.description') -%></p>
- </header>
-
- <style>
- #widget_links td, #widget_links th {
- white-space: nowrap;
- vertical-align: top;
- text-align: left;
- padding-left: 20px;
- padding-right: 20px;
- }
- </style>
-
- <div class="yui-g widget" id="widget_links">
-
- <% form_for( 'set_links', :url => { :action => 'set_links', :project_id => @project.id } ) do |form|
- links_by_key={}
- @project.links.each do |link|
- links_by_key[link.link_type]=link
- end
- %>
- <div class="yui-u first">
- <table class="data">
- <thead><tr><th >Title</th><th width="100%">URL</th></tr></thead>
-
- <tr class="even">
- <td>
- <%= image_tag("links/homepage.png") -%> <%= message('project_links.homepage') -%>
- <br/>
- <span class="note" style="margin-left: 20px">sonar.links.homepage</span>
- </td>
- <td>
- <%
- link = links_by_key['homepage'] ? links_by_key['homepage'].href : ''
- if link.starts_with?("http")
- link = "<a href=\"#{h link}\">#{h link}</a>"
- end
- %>
- <%= link -%>
- </td>
- </tr>
- <tr class="odd">
- <td>
- <%= image_tag("links/ci.png") -%> <%= message('project_links.ci') -%>
- <br/>
- <span class="note" style="margin-left: 20px">sonar.links.ci</span>
- </td>
- <td>
- <%
- link = links_by_key['ci'] ? links_by_key['ci'].href : ''
- if link.starts_with?("http")
- link = "<a href=\"#{h link}\">#{h link}</a>"
- end
- %>
- <%= link -%>
- </td>
- </tr>
- <tr class="even">
- <td>
- <%= image_tag("links/issue.png") -%> <%= message('project_links.issue') -%>
- <br/>
- <span class="note" style="margin-left: 20px">sonar.links.issue</span>
- </td>
- <td>
- <%
- link = links_by_key['issue'] ? links_by_key['issue'].href : ''
- if link.starts_with?("http")
- link = "<a href=\"#{h link}\">#{h link}</a>"
- end
- %>
- <%= link -%>
- </td>
- </tr>
- <tr class="odd">
- <td>
- <%= image_tag("links/scm.png") -%> <%= message('project_links.scm') -%>
- <br/>
- <span class="note" style="margin-left: 20px">sonar.links.scm</span>
- </td>
- <td>
- <%
- link = links_by_key['scm'] ? links_by_key['scm'].href : ''
- if link.starts_with?("http")
- link = "<a href=\"#{h link}\">#{h link}</a>"
- end
- %>
- <%= link -%>
- </td>
- </tr>
- <tr class="even">
- <td>
- <%= image_tag("links/scm_dev.png") -%> <%= message('project_links.scm_dev') -%>
- <br/>
- <span class="note" style="margin-left: 20px">sonar.links.scm_dev</span>
- </td>
- <td>
- <%
- link = links_by_key['scm_dev'] ? links_by_key['scm_dev'].href : ''
- if link.starts_with?("http")
- link = "<a href=\"#{h link}\">#{h link}</a>"
- end
- %>
- <%= link -%>
- </td>
- </tr>
-
- <% index = 0
- @project.custom_links.each do |custom_link|
- index += 1 %>
- <tr class="<%= cycle('odd','even') -%>">
- <td align="left">
- <%= image_tag("links/external.png") -%> <%= text_field_tag( "name_#{index}", h(custom_link.name), :size => 15 ) -%>
- </td>
- <td>
- <%= text_field_tag( "url_#{index}", custom_link.href, :size => 30 ) -%>
- </td>
- </tr>
- <% end %>
- <% index += 1
- for var in index..5 %>
- <tr class="<%= cycle('odd','even') -%>">
- <td align="left"><%= image_tag("links/external.png") -%> <%= text_field_tag( "name_#{var}", '', :size => 15) %></td>
- <td><%= text_field_tag( "url_#{var}", '', :size => 30 ) %></td>
- </tr>
- <% end %>
- </table>
- <br/>
- <%= submit_tag( "Save links" ) %>
- </div>
- <% end %>
- </div>
-</div>
+<% content_for :extra_script do %>
+ <script src="<%= ApplicationController.root_context -%>/js/bundles/project-admin.js?v=<%= sonar_version -%>"></script>
+<% 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
+
#------------------------------------------------------------------------------
#