Browse Source

rewrite remaining backbone modals in react

tags/7.5
Stas Vilchik 6 years ago
parent
commit
36969ad381
36 changed files with 616 additions and 1016 deletions
  1. 29
    25
      server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/ProjectKeyPage.java
  2. 10
    19
      server/sonar-web/src/main/js/api/projectLinks.ts
  3. 7
    0
      server/sonar-web/src/main/js/app/types.ts
  4. 1
    4
      server/sonar-web/src/main/js/app/utils/exposeLibraries.ts
  5. 1
    1
      server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx
  6. 2
    2
      server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx
  7. 29
    32
      server/sonar-web/src/main/js/apps/project-admin/key/Key.js
  8. 0
    87
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js
  9. 85
    0
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.tsx
  10. 65
    0
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyConfirm.tsx
  11. 0
    89
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js
  12. 75
    0
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.tsx
  13. 0
    30
      server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs
  14. 0
    41
      server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js
  15. 111
    0
      server/sonar-web/src/main/js/apps/project-admin/links/CreationModal.tsx
  16. 0
    51
      server/sonar-web/src/main/js/apps/project-admin/links/Header.js
  17. 73
    0
      server/sonar-web/src/main/js/apps/project-admin/links/Header.tsx
  18. 34
    25
      server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.tsx
  19. 5
    15
      server/sonar-web/src/main/js/apps/project-admin/links/Links.js
  20. 2
    6
      server/sonar-web/src/main/js/apps/project-admin/links/Table.js
  21. 0
    42
      server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js
  22. 0
    22
      server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs
  23. 0
    46
      server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js
  24. 0
    13
      server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs
  25. 26
    9
      server/sonar-web/src/main/js/apps/project-admin/store/actions.js
  26. 38
    27
      server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js
  27. 0
    73
      server/sonar-web/src/main/js/apps/quality-gates/views/gate-projects-view.js
  28. 0
    47
      server/sonar-web/src/main/js/components/RestartModal/index.js
  29. 0
    14
      server/sonar-web/src/main/js/components/RestartModal/templates/restarting.hbs
  30. 0
    15
      server/sonar-web/src/main/js/components/RestartModal/templates/template.hbs
  31. 0
    99
      server/sonar-web/src/main/js/components/common/modal-form.js
  32. 0
    92
      server/sonar-web/src/main/js/components/common/modals.js
  33. 0
    82
      server/sonar-web/src/main/js/components/common/selectable-collection-view.js
  34. 17
    2
      server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
  35. 1
    1
      tests/src/test/java/org/sonarqube/tests/project/ProjectKeyUpdatePageTest.java
  36. 5
    5
      tests/src/test/java/org/sonarqube/tests/project/ProjectLinksTest.java

+ 29
- 25
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/ProjectKeyPage.java View File

@@ -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;
}

+ 10
- 19
server/sonar-web/src/main/js/api/projectLinks.ts View File

@@ -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
);
}

+ 7
- 0
server/sonar-web/src/main/js/app/types.ts View File

@@ -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;

+ 1
- 4
server/sonar-web/src/main/js/app/utils/exposeLibraries.ts View File

@@ -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
};
};


+ 1
- 1
server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx View File

@@ -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;

+ 2
- 2
server/sonar-web/src/main/js/apps/overview/meta/MetaLinks.tsx View File

@@ -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 {

+ 29
- 32
server/sonar-web/src/main/js/apps/project-admin/key/Key.js View File

@@ -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>

+ 0
- 87
server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.js View File

@@ -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>
);
}
}

+ 85
- 0
server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.tsx View File

@@ -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>
);
}
}

+ 65
- 0
server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyConfirm.tsx View File

@@ -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>
);
}
}

+ 0
- 89
server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.js View File

@@ -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>
);
}
}

+ 75
- 0
server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.tsx View File

@@ -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>
);
}
}

+ 0
- 30
server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.hbs View File

@@ -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>

+ 0
- 41
server/sonar-web/src/main/js/apps/project-admin/key/views/UpdateKeyConfirmation.js View File

@@ -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
};
}
});

+ 111
- 0
server/sonar-web/src/main/js/apps/project-admin/links/CreationModal.tsx View File

@@ -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>
);
}
}

+ 0
- 51
server/sonar-web/src/main/js/apps/project-admin/links/Header.js View File

@@ -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>
);
}
}

+ 73
- 0
server/sonar-web/src/main/js/apps/project-admin/links/Header.tsx View File

@@ -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} />
)}
</>
);
}
}

server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.js → server/sonar-web/src/main/js/apps/project-admin/links/LinkRow.tsx View File

@@ -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;

+ 5
- 15
server/sonar-web/src/main/js/apps/project-admin/links/Links.js View File

@@ -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 (

+ 2
- 6
server/sonar-web/src/main/js/apps/project-admin/links/Table.js View File

@@ -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>

+ 0
- 42
server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModal.js View File

@@ -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();
});
}
});

+ 0
- 22
server/sonar-web/src/main/js/apps/project-admin/links/views/CreationModalTemplate.hbs View File

@@ -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>

+ 0
- 46
server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModal.js View File

@@ -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 };
}
});

+ 0
- 13
server/sonar-web/src/main/js/apps/project-admin/links/views/DeletionModalTemplate.hbs View File

@@ -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>

+ 26
- 9
server/sonar-web/src/main/js/apps/project-admin/store/actions.js View File

@@ -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
);
};

+ 38
- 27
server/sonar-web/src/main/js/apps/quality-gates/components/Projects.js View File

@@ -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)} />;
}
}

+ 0
- 73
server/sonar-web/src/main/js/apps/quality-gates/views/gate-projects-view.js View File

@@ -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
};
}
});

+ 0
- 47
server/sonar-web/src/main/js/components/RestartModal/index.js View File

@@ -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;

+ 0
- 14
server/sonar-web/src/main/js/components/RestartModal/templates/restarting.hbs View File

@@ -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>

+ 0
- 15
server/sonar-web/src/main/js/components/RestartModal/templates/template.hbs View File

@@ -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>

+ 0
- 99
server/sonar-web/src/main/js/components/common/modal-form.js View File

@@ -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');
}
});

+ 0
- 92
server/sonar-web/src/main/js/components/common/modals.js View File

@@ -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();
});
}
});

+ 0
- 82
server/sonar-web/src/main/js/components/common/selectable-collection-view.js View File

@@ -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();
}
}
});

+ 17
- 2
server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx View File

@@ -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}

+ 1
- 1
tests/src/test/java/org/sonarqube/tests/project/ProjectKeyUpdatePageTest.java View File

@@ -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");

+ 5
- 5
tests/src/test/java/org/sonarqube/tests/project/ProjectLinksTest.java View File

@@ -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);
}

Loading…
Cancel
Save