Переглянути джерело

SONAR-9756 Build UI for branch management (#2433)

tags/6.6-RC1
Stas Vilchik 6 роки тому
джерело
коміт
25140ec8ed
39 змінених файлів з 1455 додано та 49 видалено
  1. 2
    1
      server/sonar-web/package.json
  2. 9
    1
      server/sonar-web/src/main/js/api/branches.ts
  3. 21
    3
      server/sonar-web/src/main/js/app/components/ProjectContainer.tsx
  4. 18
    0
      server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx
  5. 4
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  6. 3
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
  7. 14
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
  8. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
  9. 23
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  10. 1
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
  11. 14
    4
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
  12. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
  13. 34
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  14. 4
    10
      server/sonar-web/src/main/js/app/components/search/Search.css
  15. 1
    1
      server/sonar-web/src/main/js/app/components/search/Search.js
  16. 2
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  17. 68
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
  18. 138
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
  19. 105
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
  20. 131
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
  21. 34
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
  22. 65
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
  23. 98
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx
  24. 95
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx
  25. 73
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
  26. 95
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
  27. 104
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap
  28. 223
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap
  29. 30
    0
      server/sonar-web/src/main/js/apps/projectBranches/routes.ts
  30. 0
    0
      server/sonar-web/src/main/js/components/common/BranchStatus.css
  31. 6
    6
      server/sonar-web/src/main/js/components/common/BranchStatus.tsx
  32. 1
    1
      server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx
  33. 0
    0
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap
  34. 6
    7
      server/sonar-web/src/main/js/components/icons-components/ChangeIcon.tsx
  35. 6
    7
      server/sonar-web/src/main/js/components/icons-components/DeleteIcon.tsx
  36. 2
    2
      server/sonar-web/src/main/js/helpers/request.ts
  37. 11
    0
      server/sonar-web/src/main/less/components/dropdowns.less
  38. 5
    0
      server/sonar-web/src/main/less/components/menu.less
  39. 6
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 1
server/sonar-web/package.json Переглянути файл

@@ -26,6 +26,7 @@
"keymaster": "1.6.2",
"lodash": "4.17.4",
"numeral": "1.5.3",
"prop-types": "15.5.10",
"rc-tooltip": "3.4.7",
"react": "15.6.1",
"react-dom": "15.6.1",
@@ -96,7 +97,6 @@
"less-loader": "4.0.4",
"postcss-loader": "2.0.6",
"prettier": "1.5.2",
"prop-types": "15.5.10",
"react-dev-utils": "3.0.0",
"react-error-overlay": "1.0.7",
"react-test-renderer": "15.6.1",
@@ -132,6 +132,7 @@
"jest": {
"coverageDirectory": "<rootDir>/target/coverage",
"coveragePathIgnorePatterns": ["<rootDir>/node_modules", "<rootDir>/tests"],
"mapCoverage": true,
"moduleFileExtensions": ["ts", "tsx", "js", "json"],
"moduleNameMapper": {
"^.+\\.(hbs|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/config/jest/FileStub.js",

+ 9
- 1
server/sonar-web/src/main/js/api/branches.ts Переглянути файл

@@ -17,9 +17,17 @@
* 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 } from '../helpers/request';
import { getJSON, post } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';

export function getBranches(project: string): Promise<any> {
return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError);
}

export function deleteBranch(project: string, branch: string): Promise<void | Response> {
return post('/api/project_branches/delete', { project, branch }).catch(throwGlobalError);
}

export function renameBranch(project: string, branch: string): Promise<void | Response> {
return post('/api/project_branches/rename', { project, branch }).catch(throwGlobalError);
}

+ 21
- 3
server/sonar-web/src/main/js/app/components/ProjectContainer.tsx Переглянути файл

@@ -83,9 +83,7 @@ export default class ProjectContainer extends React.PureComponent<Props, State>

Promise.all([getComponentNavigation(id), getComponentData(id, branch)]).then(([nav, data]) => {
const component = this.addQualifier({ ...nav, ...data });
const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK');
const branchesRequest = project ? getBranches(project.key) : Promise.resolve([]);
branchesRequest.then(branches => {
this.fetchBranches(component).then(branches => {
if (this.mounted) {
this.setState({ loading: false, branches, component });
}
@@ -93,12 +91,30 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
}, onError);
}

fetchBranches = (component: Component) => {
const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK');
return project ? getBranches(project.key) : Promise.resolve([]);
};

handleProjectChange = (changes: {}) => {
if (this.mounted) {
this.setState(state => ({ component: { ...state.component, ...changes } }));
}
};

handleBranchesChange = () => {
if (this.mounted && this.state.component) {
this.fetchBranches(this.state.component).then(
branches => {
if (this.mounted) {
this.setState({ branches });
}
},
() => {}
);
}
};

render() {
const { query } = this.props.location;
const { branches, component, loading } = this.state;
@@ -128,7 +144,9 @@ export default class ProjectContainer extends React.PureComponent<Props, State>
/>}
{React.cloneElement(this.props.children, {
branch,
branches,
component: component,
onBranchesChange: this.handleBranchesChange,
onComponentChange: this.handleProjectChange
})}
</div>

+ 18
- 0
server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx Переглянути файл

@@ -100,3 +100,21 @@ it("doesn't load branches portfolio", () => {
expect(getComponentNavigation).toBeCalledWith('portfolioKey');
});
});

it('updates branches on change', () => {
(getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
const Inner = () => <div />;
const wrapper = shallow(
<ProjectContainer location={{ query: { id: 'portfolioKey' } }}>
<Inner />
</ProjectContainer>
);
(wrapper.instance() as ProjectContainer).mounted = true;
wrapper.setState({
branches: [{ isMain: true }],
component: { breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: 'TRK' }] },
loading: false
});
(wrapper.find(Inner).prop('onBranchesChange') as Function)();
expect(getBranches).toBeCalledWith('projectKey');
});

+ 4
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx Переглянути файл

@@ -102,6 +102,8 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
<ComponentNavBranch
branches={this.props.branches}
currentBranch={this.props.currentBranch}
// to close dropdown on any location change
location={this.props.location}
project={this.props.component}
/>

@@ -116,6 +118,8 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
branch={this.props.currentBranch}
component={this.props.component}
conf={this.props.conf}
// to re-render selected menu item
location={this.props.location}
/>
</ContextNavBar>
);

+ 3
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx Переглянути файл

@@ -33,6 +33,7 @@ import BubblePopupHelper from '../../../../components/common/BubblePopupHelper';
interface Props {
branches: Branch[];
currentBranch: Branch;
location?: any;
project: Component;
}

@@ -61,7 +62,8 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
componentWillReceiveProps(nextProps: Props) {
if (
nextProps.project !== this.props.project ||
nextProps.currentBranch !== this.props.currentBranch
nextProps.currentBranch !== this.props.currentBranch ||
nextProps.location !== this.props.location
) {
this.setState({ dropdownOpen: false, singleBranchPopupOpen: false });
}

+ 14
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx Переглянути файл

@@ -28,6 +28,7 @@ import {
} from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
import { getProjectBranchUrl } from '../../../../helpers/urls';
import { Link } from 'react-router';

interface Props {
branches: Branch[];
@@ -179,17 +180,29 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
});

return (
<ul className="menu">
<ul className="menu menu-vertically-limited">
{menu}
</ul>
);
};

render() {
const { project } = this.props;
const showManageLink =
project.qualifier === 'TRK' && project.configuration && project.configuration.showSettings;

return (
<div className="dropdown-menu dropdown-menu-shadow" ref={node => (this.node = node)}>
{this.renderSearch()}
{this.renderBranchesList()}
{showManageLink &&
<div className="dropdown-bottom-hint text-right">
<Link
className="text-muted"
to={{ pathname: '/project/branches', query: { id: project.key } }}>
{translate('branches.manage')}
</Link>
</div>}
</div>
);
}

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx Переглянути файл

@@ -20,7 +20,7 @@
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
import BranchStatus from './BranchStatus';
import BranchStatus from '../../../../components/common/BranchStatus';
import { Branch, Component } from '../../../types';
import BranchIcon from '../../../../components/icons-components/BranchIcon';
import { isShortLivingBranch } from '../../../../helpers/branches';

+ 23
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx Переглянути файл

@@ -20,6 +20,7 @@
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
import * as PropTypes from 'prop-types';
import { Branch, Component, ComponentExtension, ComponentConfiguration } from '../../../types';
import NavBarTabs from '../../../../components/nav/NavBarTabs';
import { isShortLivingBranch, getBranchName } from '../../../../helpers/branches';
@@ -27,6 +28,7 @@ import { translate } from '../../../../helpers/l10n';

const SETTINGS_URLS = [
'/project/admin',
'/project/branches',
'/project/settings',
'/project/quality_profiles',
'/project/quality_gate',
@@ -43,9 +45,14 @@ interface Props {
branch: Branch;
component: Component;
conf: ComponentConfiguration;
location?: any;
}

export default class ComponentNavMenu extends React.PureComponent<Props> {
static contextTypes = {
branchesEnabled: PropTypes.bool.isRequired
};

isProject() {
return this.props.component.qualifier === 'TRK';
}
@@ -196,6 +203,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
renderAdministrationLinks() {
return [
this.renderSettingsLink(),
this.renderBranchesLink(),
this.renderProfilesLink(),
this.renderQualityGateLink(),
this.renderCustomMeasuresLink(),
@@ -223,6 +231,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
);
}

renderBranchesLink() {
if (!this.context.branchesEnabled || !this.isProject() || !this.props.conf.showSettings) {
return null;
}
return (
<li key="branches">
<Link
to={{ pathname: '/project/branches', query: { id: this.props.component.key } }}
activeClassName="active">
{translate('project_branches.page')}
</Link>
</li>
);
}

renderProfilesLink() {
if (!this.props.conf.showQualityProfiles) {
return null;

+ 1
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx Переглянути файл

@@ -19,7 +19,7 @@
*/
import * as React from 'react';
import IncrementalBadge from './IncrementalBadge';
import BranchStatus from './BranchStatus';
import BranchStatus from '../../../../components/common/BranchStatus';
import { Branch, Component, ComponentConfiguration } from '../../../types';
import Tooltip from '../../../../components/controls/Tooltip';
import PendingIcon from '../../../../components/icons-components/PendingIcon';

+ 14
- 4
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx Переглянути файл

@@ -44,7 +44,10 @@ it('should work with extensions', () => {
extensions: [{ key: 'foo', name: 'Foo' }]
};
expect(
shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />)
shallow(
<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />,
{ context: { branchesEnabled: true } }
)
).toMatchSnapshot();
});

@@ -62,7 +65,10 @@ it('should work with multiple extensions', () => {
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]
};
expect(
shallow(<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />)
shallow(
<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />,
{ context: { branchesEnabled: true } }
)
).toMatchSnapshot();
});

@@ -77,7 +83,9 @@ it('should work for short-living branches', () => {
const component = { key: 'foo', qualifier: 'TRK' } as Component;
const conf = { showSettings: true };
expect(
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />)
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />, {
context: { branchesEnabled: true }
})
).toMatchSnapshot();
});

@@ -86,6 +94,8 @@ it('should work for long-living branches', () => {
const component = { key: 'foo', qualifier: 'TRK' } as Component;
const conf = { showSettings: true };
expect(
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />)
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />, {
context: { branchesEnabled: true }
})
).toMatchSnapshot();
});

+ 2
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap Переглянути файл

@@ -25,7 +25,7 @@ exports[`renders list 1`] = `
/>
</div>
<ul
className="menu"
className="menu menu-vertically-limited"
>
<ComponentNavBranchesMenuItem
branch={
@@ -165,7 +165,7 @@ exports[`searches 1`] = `
/>
</div>
<ul
className="menu"
className="menu menu-vertically-limited"
>
<ComponentNavBranchesMenuItem
branch={

+ 34
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap Переглянути файл

@@ -266,6 +266,23 @@ exports[`should work with extensions 1`] = `
project_settings.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/branches",
"query": Object {
"id": "foo",
},
}
}
>
project_branches.page
</Link>
</li>
<li>
<Link
activeClassName="active"
@@ -470,6 +487,23 @@ exports[`should work with multiple extensions 1`] = `
project_settings.page
</Link>
</li>
<li>
<Link
activeClassName="active"
onlyActiveOnIndex={false}
style={Object {}}
to={
Object {
"pathname": "/project/branches",
"query": Object {
"id": "foo",
},
}
}
>
project_branches.page
</Link>
</li>
<li>
<Link
activeClassName="active"

+ 4
- 10
server/sonar-web/src/main/js/app/components/search/Search.css Переглянути файл

@@ -72,16 +72,6 @@
left: -5px;
}

.navbar-search-shortcut-hint {
line-height: 16px;
margin-top: 5px;
padding: 5px 10px;
border-top: 1px solid #e6e6e6;
background-color: #f3f3f3;
color: #777;
font-size: 11px;
}

.navbar-search-no-results {
margin-top: 4px;
padding: 5px 10px;
@@ -94,3 +84,7 @@
overflow-y: auto;
overflow-x: hidden;
}

.global-navbar-search-dropdown .dropdown-bottom-hint {
margin-bottom: 0;
}

+ 1
- 1
server/sonar-web/src/main/js/app/components/search/Search.js Переглянути файл

@@ -367,7 +367,7 @@ export default class Search extends React.PureComponent {
results={this.state.results}
selected={this.state.selected}
/>
<div className="navbar-search-shortcut-hint">
<div className="dropdown-bottom-hint">
<div className="pull-right">
<ClockIcon className="little-spacer-right" size={12} />
{translate('recently_browsed')}

+ 2
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.js Переглянути файл

@@ -55,6 +55,7 @@ import organizationsRoutes from '../../apps/organizations/routes';
import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
import projectAdminRoutes from '../../apps/project-admin/routes';
import projectBranchesRoutes from '../../apps/projectBranches/routes';
import projectsRoutes from '../../apps/projects/routes';
import projectsManagementRoutes from '../../apps/projectsManagement/routes';
import qualityGatesRoutes from '../../apps/quality-gates/routes';
@@ -187,6 +188,7 @@ const startReactApp = () => {
component={ProjectPageExtension}
/>
<Route path="background_tasks" childRoutes={backgroundTasksRoutes} />
<Route path="branches" childRoutes={projectBranchesRoutes} />
<Route path="issues" childRoutes={issuesRoutes} />
<Route path="settings" childRoutes={settingsRoutes} />
{projectAdminRoutes}

+ 68
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx Переглянути файл

@@ -0,0 +1,68 @@
/*
* 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 * as React from 'react';
import BranchRow from './BranchRow';
import { Branch } from '../../../app/types';
import { sortBranchesAsTree } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';

interface Props {
branches: Branch[];
component: { key: string };
onBranchesChange: () => void;
}

export default function App({ branches, component, onBranchesChange }: Props) {
return (
<div className="page page-limited">
<header className="page-header">
<h1 className="page-title">
{translate('project_branches.page')}
</h1>
</header>

<table className="data zebra zebra-hover">
<thead>
<tr>
<th>
{translate('branch')}
</th>
<th className="text-right">
{translate('status')}
</th>
<th className="text-right">
{translate('actions')}
</th>
</tr>
</thead>
<tbody>
{sortBranchesAsTree(branches).map(branch =>
<BranchRow
branch={branch}
component={component.key}
key={branch.name}
onChange={onBranchesChange}
/>
)}
</tbody>
</table>
</div>
);
}

+ 138
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx Переглянути файл

@@ -0,0 +1,138 @@
/*
* 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 * as React from 'react';
import { Branch } from '../../../app/types';
import * as classNames from 'classnames';
import DeleteBranchModal from './DeleteBranchModal';
import BranchStatus from '../../../components/common/BranchStatus';
import BranchIcon from '../../../components/icons-components/BranchIcon';
import { isShortLivingBranch } from '../../../helpers/branches';
import ChangeIcon from '../../../components/icons-components/ChangeIcon';
import DeleteIcon from '../../../components/icons-components/DeleteIcon';
import { translate } from '../../../helpers/l10n';
import Tooltip from '../../../components/controls/Tooltip';
import RenameBranchModal from './RenameBranchModal';

interface Props {
branch: Branch;
component: string;
onChange: () => void;
}

interface State {
deleting: boolean;
renaming: boolean;
}

export default class BranchRow extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { deleting: false, renaming: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleDeleteClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ deleting: true });
};

handleDeletingStop = () => {
this.setState({ deleting: false });
};

handleRenameClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ renaming: true });
};

handleChange = () => {
if (this.mounted) {
this.setState({ deleting: false, renaming: false });
this.props.onChange();
}
};

handleRenamingStop = () => {
this.setState({ renaming: false });
};

render() {
const { branch, component } = this.props;

return (
<tr>
<td>
<BranchIcon
branch={branch}
className={classNames('little-spacer-right', {
'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
})}
/>
{branch.name}
</td>
<td className="thin nowrap text-right">
<BranchStatus branch={branch} />
</td>
<td className="thin nowrap text-right">
{branch.isMain
? <Tooltip overlay={translate('branches.rename')}>
<a
className="js-rename link-no-underline"
href="#"
onClick={this.handleRenameClick}>
<ChangeIcon />
</a>
</Tooltip>
: <Tooltip overlay={translate('branches.delete')}>
<a
className="js-delete link-no-underline"
href="#"
onClick={this.handleDeleteClick}>
<DeleteIcon />
</a>
</Tooltip>}
</td>

{this.state.deleting &&
<DeleteBranchModal
branch={branch}
component={component}
onClose={this.handleDeletingStop}
onDelete={this.handleChange}
/>}

{this.state.renaming &&
<RenameBranchModal
branch={branch}
component={component}
onClose={this.handleRenamingStop}
onRename={this.handleChange}
/>}
</tr>
);
}
}

+ 105
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx Переглянути файл

@@ -0,0 +1,105 @@
/*
* 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 * as React from 'react';
import Modal from 'react-modal';
import { deleteBranch } from '../../../api/branches';
import { Branch } from '../../../app/types';
import { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
branch: Branch;
component: string;
onClose: () => void;
onDelete: () => void;
}

interface State {
loading: boolean;
}

export default class DeleteBranchModal extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
this.setState({ loading: true });
deleteBranch(this.props.component, this.props.branch.name).then(
() => {
if (this.mounted) {
this.setState({ loading: false });
this.props.onDelete();
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.onClose();
};

render() {
const { branch } = this.props;
const header = translate('branches.delete');

return (
<Modal
isOpen={true}
contentLabel={header}
className="modal"
overlayClassName="modal-overlay"
onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>
{header}
</h2>
</header>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
{translateWithParameters('branches.delete.are_you_sure', branch.name)}
</div>
<footer className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
<button className="button-red" disabled={this.state.loading} type="submit">
{translate('delete')}
</button>
<a href="#" onClick={this.handleCancelClick}>
{translate('cancel')}
</a>
</footer>
</form>
</Modal>
);
}
}

+ 131
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx Переглянути файл

@@ -0,0 +1,131 @@
/*
* 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 * as React from 'react';
import Modal from 'react-modal';
import { renameBranch } from '../../../api/branches';
import { Branch } from '../../../app/types';
import { translate } from '../../../helpers/l10n';

interface Props {
branch: Branch;
component: string;
onClose: () => void;
onRename: () => void;
}

interface State {
loading: boolean;
name?: string;
}

export default class RenameBranchModal extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { loading: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
if (!this.state.name) {
return;
}
this.setState({ loading: true });
renameBranch(this.props.component, this.state.name).then(
() => {
if (this.mounted) {
this.setState({ loading: false });
this.props.onRename();
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.onClose();
};

handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ name: event.currentTarget.value });
};

render() {
const { branch } = this.props;
const header = translate('branches.rename');
const submitDisabled =
this.state.loading || !this.state.name || this.state.name === branch.name;

return (
<Modal
isOpen={true}
contentLabel={header}
className="modal"
overlayClassName="modal-overlay"
onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>
{header}
</h2>
</header>
<form onSubmit={this.handleSubmit}>
<div className="modal-body">
<div className="modal-field">
<label htmlFor="rename-branch-name">
{translate('new_name')}
<em className="mandatory">*</em>
</label>
<input
autoFocus={true}
id="rename-branch-name"
maxLength={100}
name="name"
onChange={this.handleNameChange}
required={true}
size={50}
type="text"
value={this.state.name != undefined ? this.state.name : branch.name}
/>
</div>
</div>
<footer className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
<button disabled={submitDisabled} type="submit">
{translate('rename')}
</button>
<a href="#" onClick={this.handleCancelClick}>
{translate('cancel')}
</a>
</footer>
</form>
</Modal>
);
}
}

+ 34
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx Переглянути файл

@@ -0,0 +1,34 @@
/*
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import App from '../App';
import { Branch, BranchType } from '../../../../app/types';

it('renders sorted list of branches', () => {
const branches: Branch[] = [
{ isMain: true, name: 'master' },
{ isMain: false, name: 'branch-1.0', type: BranchType.LONG },
{ isMain: false, name: 'branch-1.0', mergeBranch: 'master', type: BranchType.SHORT }
];
expect(
shallow(<App branches={branches} component={{ key: 'foo' }} onBranchesChange={jest.fn()} />)
).toMatchSnapshot();
});

+ 65
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx Переглянути файл

@@ -0,0 +1,65 @@
/*
* 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 * as React from 'react';
import { shallow } from 'enzyme';
import BranchRow from '../BranchRow';
import { MainBranch, ShortLivingBranch, BranchType } from '../../../../app/types';
import { click } from '../../../../helpers/testUtils';

const mainBranch: MainBranch = { isMain: true, name: 'master' };

const shortBranch: ShortLivingBranch = {
isMain: false,
name: 'feature',
mergeBranch: 'foo',
type: BranchType.SHORT
};

it('renders main branch', () => {
expect(shallowRender(mainBranch)).toMatchSnapshot();
});

it('renders short-living branch', () => {
expect(shallowRender(shortBranch)).toMatchSnapshot();
});

it('renames main branch', () => {
const onChange = jest.fn();
const wrapper = shallowRender(mainBranch, onChange);

click(wrapper.find('.js-rename'));
(wrapper.find('RenameBranchModal').prop('onRename') as Function)();
expect(onChange).toBeCalled();
});

it('deletes short-living branch', () => {
const onChange = jest.fn();
const wrapper = shallowRender(shortBranch, onChange);

click(wrapper.find('.js-delete'));
(wrapper.find('DeleteBranchModal').prop('onDelete') as Function)();
expect(onChange).toBeCalled();
});

function shallowRender(branch: MainBranch | ShortLivingBranch, onChange: () => void = jest.fn()) {
const wrapper = shallow(<BranchRow branch={branch} component="foo" onChange={onChange} />);
(wrapper.instance() as any).mounted = true;
return wrapper;
}

+ 98
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx Переглянути файл

@@ -0,0 +1,98 @@
/*
* 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.
*/
jest.mock('../../../../api/branches', () => ({ deleteBranch: jest.fn() }));

import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import DeleteBranchModal from '../DeleteBranchModal';
import { ShortLivingBranch, BranchType } from '../../../../app/types';
import { submit, doAsync, click } from '../../../../helpers/testUtils';
import { deleteBranch } from '../../../../api/branches';

beforeEach(() => {
(deleteBranch as jest.Mock<any>).mockClear();
});

it('renders', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
wrapper.setState({ loading: true });
expect(wrapper).toMatchSnapshot();
});

it('deletes branch', () => {
(deleteBranch as jest.Mock<any>).mockImplementation(() => Promise.resolve());
const onDelete = jest.fn();
const wrapper = shallowRender(onDelete);

submitForm(wrapper);

return doAsync().then(() => {
wrapper.update();
expect(wrapper.state().loading).toBe(false);
expect(onDelete).toBeCalled();
expect(deleteBranch).toBeCalledWith('foo', 'feature');
});
});

it('cancels', () => {
const onClose = jest.fn();
const wrapper = shallowRender(jest.fn(), onClose);

click(wrapper.find('a'));

return doAsync().then(() => {
expect(onClose).toBeCalled();
});
});

it('stops loading on WS error', () => {
(deleteBranch as jest.Mock<any>).mockImplementation(() => Promise.reject(null));
const onDelete = jest.fn();
const wrapper = shallowRender(onDelete);

submitForm(wrapper);

return doAsync().then(() => {
wrapper.update();
expect(wrapper.state().loading).toBe(false);
expect(onDelete).not.toBeCalled();
expect(deleteBranch).toBeCalledWith('foo', 'feature');
});
});

function shallowRender(onDelete: () => void = jest.fn(), onClose: () => void = jest.fn()) {
const branch: ShortLivingBranch = {
isMain: false,
name: 'feature',
mergeBranch: 'master',
type: BranchType.SHORT
};
const wrapper = shallow(
<DeleteBranchModal branch={branch} component="foo" onClose={onClose} onDelete={onDelete} />
);
(wrapper.instance() as any).mounted = true;
return wrapper;
}

function submitForm(wrapper: ShallowWrapper<any, any>) {
submit(wrapper.find('form'));
expect(wrapper.state().loading).toBe(true);
}

+ 95
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx Переглянути файл

@@ -0,0 +1,95 @@
/*
* 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.
*/
jest.mock('../../../../api/branches', () => ({ renameBranch: jest.fn() }));

import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import RenameBranchModal from '../RenameBranchModal';
import { MainBranch } from '../../../../app/types';
import { submit, doAsync, click, change } from '../../../../helpers/testUtils';
import { renameBranch } from '../../../../api/branches';

beforeEach(() => {
(renameBranch as jest.Mock<any>).mockClear();
});

it('renders', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
wrapper.setState({ name: 'dev' });
expect(wrapper).toMatchSnapshot();
wrapper.setState({ loading: true });
expect(wrapper).toMatchSnapshot();
});

it('renames branch', () => {
(renameBranch as jest.Mock<any>).mockImplementation(() => Promise.resolve());
const onRename = jest.fn();
const wrapper = shallowRender(onRename);

fillAndSubmit(wrapper);

return doAsync().then(() => {
wrapper.update();
expect(wrapper.state().loading).toBe(false);
expect(onRename).toBeCalled();
expect(renameBranch).toBeCalledWith('foo', 'dev');
});
});

it('cancels', () => {
const onClose = jest.fn();
const wrapper = shallowRender(jest.fn(), onClose);

click(wrapper.find('a'));

return doAsync().then(() => {
expect(onClose).toBeCalled();
});
});

it('stops loading on WS error', () => {
(renameBranch as jest.Mock<any>).mockImplementation(() => Promise.reject(null));
const onRename = jest.fn();
const wrapper = shallowRender(onRename);

fillAndSubmit(wrapper);

return doAsync().then(() => {
wrapper.update();
expect(wrapper.state().loading).toBe(false);
expect(onRename).not.toBeCalled();
});
});

function shallowRender(onRename: () => void = jest.fn(), onClose: () => void = jest.fn()) {
const branch: MainBranch = { isMain: true, name: 'master' };
const wrapper = shallow(
<RenameBranchModal branch={branch} component="foo" onClose={onClose} onRename={onRename} />
);
(wrapper.instance() as any).mounted = true;
return wrapper;
}

function fillAndSubmit(wrapper: ShallowWrapper<any, any>) {
change(wrapper.find('input'), 'dev');
submit(wrapper.find('form'));
expect(wrapper.state().loading).toBe(true);
}

+ 73
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap Переглянути файл

@@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders sorted list of branches 1`] = `
<div
className="page page-limited"
>
<header
className="page-header"
>
<h1
className="page-title"
>
project_branches.page
</h1>
</header>
<table
className="data zebra zebra-hover"
>
<thead>
<tr>
<th>
branch
</th>
<th
className="text-right"
>
status
</th>
<th
className="text-right"
>
actions
</th>
</tr>
</thead>
<tbody>
<BranchRow
branch={
Object {
"isMain": true,
"name": "master",
}
}
component="foo"
onChange={[Function]}
/>
<BranchRow
branch={
Object {
"isMain": false,
"mergeBranch": "master",
"name": "branch-1.0",
"type": "SHORT",
}
}
component="foo"
onChange={[Function]}
/>
<BranchRow
branch={
Object {
"isMain": false,
"name": "branch-1.0",
"type": "LONG",
}
}
component="foo"
onChange={[Function]}
/>
</tbody>
</table>
</div>
`;

+ 95
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap Переглянути файл

@@ -0,0 +1,95 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders main branch 1`] = `
<tr>
<td>
<BranchIcon
branch={
Object {
"isMain": true,
"name": "master",
}
}
className="little-spacer-right"
/>
master
</td>
<td
className="thin nowrap text-right"
>
<BranchStatus
branch={
Object {
"isMain": true,
"name": "master",
}
}
/>
</td>
<td
className="thin nowrap text-right"
>
<Tooltip
overlay="branches.rename"
placement="bottom"
>
<a
className="js-rename link-no-underline"
href="#"
onClick={[Function]}
>
<ChangeIcon />
</a>
</Tooltip>
</td>
</tr>
`;

exports[`renders short-living branch 1`] = `
<tr>
<td>
<BranchIcon
branch={
Object {
"isMain": false,
"mergeBranch": "foo",
"name": "feature",
"type": "SHORT",
}
}
className="little-spacer-right big-spacer-left"
/>
feature
</td>
<td
className="thin nowrap text-right"
>
<BranchStatus
branch={
Object {
"isMain": false,
"mergeBranch": "foo",
"name": "feature",
"type": "SHORT",
}
}
/>
</td>
<td
className="thin nowrap text-right"
>
<Tooltip
overlay="branches.delete"
placement="bottom"
>
<a
className="js-delete link-no-underline"
href="#"
onClick={[Function]}
>
<DeleteIcon />
</a>
</Tooltip>
</td>
</tr>
`;

+ 104
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap Переглянути файл

@@ -0,0 +1,104 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<Modal
ariaHideApp={true}
bodyOpenClassName="ReactModal__Body--open"
className="modal"
closeTimeoutMS={0}
contentLabel="branches.delete"
isOpen={true}
onRequestClose={[Function]}
overlayClassName="modal-overlay"
parentSelector={[Function]}
portalClassName="ReactModalPortal"
shouldCloseOnOverlayClick={true}
>
<header
className="modal-head"
>
<h2>
branches.delete
</h2>
</header>
<form
onSubmit={[Function]}
>
<div
className="modal-body"
>
branches.delete.are_you_sure.feature
</div>
<footer
className="modal-foot"
>
<button
className="button-red"
disabled={false}
type="submit"
>
delete
</button>
<a
href="#"
onClick={[Function]}
>
cancel
</a>
</footer>
</form>
</Modal>
`;

exports[`renders 2`] = `
<Modal
ariaHideApp={true}
bodyOpenClassName="ReactModal__Body--open"
className="modal"
closeTimeoutMS={0}
contentLabel="branches.delete"
isOpen={true}
onRequestClose={[Function]}
overlayClassName="modal-overlay"
parentSelector={[Function]}
portalClassName="ReactModalPortal"
shouldCloseOnOverlayClick={true}
>
<header
className="modal-head"
>
<h2>
branches.delete
</h2>
</header>
<form
onSubmit={[Function]}
>
<div
className="modal-body"
>
branches.delete.are_you_sure.feature
</div>
<footer
className="modal-foot"
>
<i
className="spinner spacer-right"
/>
<button
className="button-red"
disabled={true}
type="submit"
>
delete
</button>
<a
href="#"
onClick={[Function]}
>
cancel
</a>
</footer>
</form>
</Modal>
`;

+ 223
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap Переглянути файл

@@ -0,0 +1,223 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<Modal
ariaHideApp={true}
bodyOpenClassName="ReactModal__Body--open"
className="modal"
closeTimeoutMS={0}
contentLabel="branches.rename"
isOpen={true}
onRequestClose={[Function]}
overlayClassName="modal-overlay"
parentSelector={[Function]}
portalClassName="ReactModalPortal"
shouldCloseOnOverlayClick={true}
>
<header
className="modal-head"
>
<h2>
branches.rename
</h2>
</header>
<form
onSubmit={[Function]}
>
<div
className="modal-body"
>
<div
className="modal-field"
>
<label
htmlFor="rename-branch-name"
>
new_name
<em
className="mandatory"
>
*
</em>
</label>
<input
autoFocus={true}
id="rename-branch-name"
maxLength={100}
name="name"
onChange={[Function]}
required={true}
size={50}
type="text"
value="master"
/>
</div>
</div>
<footer
className="modal-foot"
>
<button
disabled={true}
type="submit"
>
rename
</button>
<a
href="#"
onClick={[Function]}
>
cancel
</a>
</footer>
</form>
</Modal>
`;

exports[`renders 2`] = `
<Modal
ariaHideApp={true}
bodyOpenClassName="ReactModal__Body--open"
className="modal"
closeTimeoutMS={0}
contentLabel="branches.rename"
isOpen={true}
onRequestClose={[Function]}
overlayClassName="modal-overlay"
parentSelector={[Function]}
portalClassName="ReactModalPortal"
shouldCloseOnOverlayClick={true}
>
<header
className="modal-head"
>
<h2>
branches.rename
</h2>
</header>
<form
onSubmit={[Function]}
>
<div
className="modal-body"
>
<div
className="modal-field"
>
<label
htmlFor="rename-branch-name"
>
new_name
<em
className="mandatory"
>
*
</em>
</label>
<input
autoFocus={true}
id="rename-branch-name"
maxLength={100}
name="name"
onChange={[Function]}
required={true}
size={50}
type="text"
value="dev"
/>
</div>
</div>
<footer
className="modal-foot"
>
<button
disabled={false}
type="submit"
>
rename
</button>
<a
href="#"
onClick={[Function]}
>
cancel
</a>
</footer>
</form>
</Modal>
`;

exports[`renders 3`] = `
<Modal
ariaHideApp={true}
bodyOpenClassName="ReactModal__Body--open"
className="modal"
closeTimeoutMS={0}
contentLabel="branches.rename"
isOpen={true}
onRequestClose={[Function]}
overlayClassName="modal-overlay"
parentSelector={[Function]}
portalClassName="ReactModalPortal"
shouldCloseOnOverlayClick={true}
>
<header
className="modal-head"
>
<h2>
branches.rename
</h2>
</header>
<form
onSubmit={[Function]}
>
<div
className="modal-body"
>
<div
className="modal-field"
>
<label
htmlFor="rename-branch-name"
>
new_name
<em
className="mandatory"
>
*
</em>
</label>
<input
autoFocus={true}
id="rename-branch-name"
maxLength={100}
name="name"
onChange={[Function]}
required={true}
size={50}
type="text"
value="dev"
/>
</div>
</div>
<footer
className="modal-foot"
>
<i
className="spinner spacer-right"
/>
<button
disabled={true}
type="submit"
>
rename
</button>
<a
href="#"
onClick={[Function]}
>
cancel
</a>
</footer>
</form>
</Modal>
`;

+ 30
- 0
server/sonar-web/src/main/js/apps/projectBranches/routes.ts Переглянути файл

@@ -0,0 +1,30 @@
/*
* 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 { RouterState, IndexRouteProps } from 'react-router';

const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
import('./components/App').then(i => callback(null, { component: (i as any).default }));
}
}
];

export default routes;

server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css → server/sonar-web/src/main/js/components/common/BranchStatus.css Переглянути файл


server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx → server/sonar-web/src/main/js/components/common/BranchStatus.tsx Переглянути файл

@@ -19,12 +19,12 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { Branch } from '../../../types';
import Level from '../../../../components/ui/Level';
import BugIcon from '../../../../components/icons-components/BugIcon';
import CodeSmellIcon from '../../../../components/icons-components/CodeSmellIcon';
import VulnerabilityIcon from '../../../../components/icons-components/VulnerabilityIcon';
import { isShortLivingBranch } from '../../../../helpers/branches';
import { Branch } from '../../app/types';
import Level from '../ui/Level';
import BugIcon from '../icons-components/BugIcon';
import CodeSmellIcon from '../icons-components/CodeSmellIcon';
import VulnerabilityIcon from '../icons-components/VulnerabilityIcon';
import { isShortLivingBranch } from '../../helpers/branches';
import './BranchStatus.css';

interface Props {

server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx → server/sonar-web/src/main/js/components/common/__tests__/BranchStatus-test.tsx Переглянути файл

@@ -20,7 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import BranchStatus from '../BranchStatus';
import { BranchType, LongLivingBranch } from '../../../../types';
import { BranchType, LongLivingBranch } from '../../../app/types';

it('renders status of short-living branches', () => {
checkShort(0, 0, 0);

server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap → server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap Переглянути файл


server/sonar-web/src/main/js/components/icons-components/ChangeIcon.js → server/sonar-web/src/main/js/components/icons-components/ChangeIcon.tsx Переглянути файл

@@ -17,15 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';

/*::
type Props = { className?: string, size?: number };
*/
interface Props {
className?: string;
size?: number;
}

export default function ChangeIcon({ className, size = 12 } /*: Props */) {
/* eslint-disable max-len */
export default function ChangeIcon({ className, size = 12 }: Props) {
return (
<svg
className={className}

server/sonar-web/src/main/js/components/icons-components/DeleteIcon.js → server/sonar-web/src/main/js/components/icons-components/DeleteIcon.tsx Переглянути файл

@@ -17,15 +17,14 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
import React from 'react';
import * as React from 'react';

/*::
type Props = { className?: string, size?: number };
*/
interface Props {
className?: string;
size?: number;
}

export default function DeleteIcon({ className, size = 12 } /*: Props */) {
/* eslint-disable max-len */
export default function DeleteIcon({ className, size = 12 }: Props) {
return (
<svg
className={className}

+ 2
- 2
server/sonar-web/src/main/js/helpers/request.ts Переглянути файл

@@ -170,10 +170,10 @@ export function postJSON(url: string, data?: RequestData): Promise<any> {
* Shortcut to do a POST request
*/
export function post(url: string, data?: RequestData): Promise<void> {
return new Promise(resolve => {
return new Promise((resolve, reject) => {
request(url).setMethod('POST').setData(data).submit().then(checkStatus).then(() => {
resolve();
});
}, reject);
});
}


+ 11
- 0
server/sonar-web/src/main/less/components/dropdowns.less Переглянути файл

@@ -99,3 +99,14 @@
top: 0;
z-index: (1000 - 10);
}

.dropdown-bottom-hint {
line-height: 16px;
margin-top: 5px;
margin-bottom: -5px;
padding: 5px 10px;
border-top: 1px solid #e6e6e6;
background-color: #f3f3f3;
color: #777;
font-size: 11px;
}

+ 5
- 0
server/sonar-web/src/main/less/components/menu.less Переглянути файл

@@ -80,6 +80,11 @@
}
}

.menu-vertically-limited {
max-height: 300px;
overflow-y: auto;
}

.menu-footer > a > span {
border-bottom: 1px solid @darkGrey;
color: @secondFontColor;

+ 6
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -112,6 +112,7 @@ name=Name
name_too_long_x=Name is too long (maximum is {0} characters)
navigation=Navigation
never=Never
new_name=New name
none=None
no_tags=No tags
off=Off
@@ -587,6 +588,7 @@ portfolio_deletion.page.description=Delete this portfolio from SonarQube. Compon
application_deletion.page.description=Delete this application from SonarQube. Application projects will not be deleted. This operation cannot be undone.
provisioning.page=Provisioning
provisioning.page.description=Use this page to initialize projects if you would like to configure them before the first analysis. Once a project is provisioned, you have access to perform all project configurations on it.
project_branches.page=Branches

#------------------------------------------------------------------------------
#
@@ -3164,3 +3166,7 @@ branches.learn_how_to_analyze=Learn how to analyze branches in SonarQube
branches.learn_how_to_analyze.text=Quickly setup branch analysis and get separate insights for each of your branches and pull requests.
branches.no_support.header=Get the most out of SonarQube with branches analysis
branches.no_support.header.text=Analyze each branch of your project separately with our Developer Pack.
branches.delete=Delete Branch
branches.delete.are_you_sure=Are you sure you want to delete branch "{0}"?
branches.rename=Rename Branch
branches.manage=Manage branches

Завантаження…
Відмінити
Зберегти