Browse Source

revert changes of branches administration

tags/6.6-RC1
Stas Vilchik 6 years ago
parent
commit
3fe5d569ca
29 changed files with 661 additions and 181 deletions
  1. 1
    1
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  2. 17
    0
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  3. 0
    2
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
  4. 0
    2
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
  5. 17
    6
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
  6. 26
    103
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
  7. 21
    0
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  8. 2
    4
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx
  9. 1
    10
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
  10. 4
    7
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
  11. 0
    20
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx
  12. 0
    7
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
  13. 51
    0
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
  14. 2
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.js
  15. 60
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx
  16. 139
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx
  17. 3
    4
      server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx
  18. 3
    4
      server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx
  19. 34
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx
  20. 65
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/BranchRow-test.tsx
  21. 4
    4
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx
  22. 4
    4
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx
  23. 73
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap
  24. 100
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap
  25. 0
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap
  26. 0
    0
      server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap
  27. 30
    0
      server/sonar-web/src/main/js/apps/projectBranches/routes.ts
  28. 1
    1
      server/sonar-web/src/main/js/apps/settings/components/App.js
  29. 3
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 1
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View File

@@ -137,7 +137,6 @@ export default class ComponentContainer extends React.PureComponent<Props, State
currentBranch={branch}
component={component}
location={this.props.location}
onBranchesChange={this.handleBranchesChange}
/>
)}
{loading ? (
@@ -149,6 +148,7 @@ export default class ComponentContainer extends React.PureComponent<Props, State
branch,
branches,
component,
onBranchesChange: this.handleBranchesChange,
onComponentChange: this.handleComponentChange
})
)}

+ 17
- 0
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx View File

@@ -106,3 +106,20 @@ it("doesn't load branches portfolio", () => {
expect(wrapper.find(Inner).exists()).toBeTruthy();
});
});

it('updates branches on change', () => {
(getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
const wrapper = shallow(
<ComponentContainer location={{ query: { id: 'portfolioKey' } }}>
<Inner />
</ComponentContainer>
);
(wrapper.instance() as ComponentContainer).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');
});

+ 0
- 2
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx View File

@@ -35,7 +35,6 @@ interface Props {
currentBranch?: Branch;
component: Component;
location: {};
onBranchesChange: () => void;
}

interface State {
@@ -106,7 +105,6 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
currentBranch={this.props.currentBranch}
// to close dropdown on any location change
location={this.props.location}
onBranchesChange={this.props.onBranchesChange}
/>
)}


+ 0
- 2
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx View File

@@ -35,7 +35,6 @@ interface Props {
component: Component;
currentBranch: Branch;
location?: any;
onBranchesChange: () => void;
}

interface State {
@@ -128,7 +127,6 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State
canAdmin={configuration && configuration.showSettings}
component={this.props.component}
currentBranch={this.props.currentBranch}
onBranchesChange={this.props.onBranchesChange}
onClose={this.closeDropdown}
/>
) : null;

+ 17
- 6
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx View File

@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { Link } from 'react-router';
import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem';
import { Branch, Component } from '../../../types';
import {
@@ -35,7 +36,6 @@ interface Props {
canAdmin?: boolean;
component: Component;
currentBranch: Branch;
onBranchesChange: () => void;
onClose: () => void;
}

@@ -66,9 +66,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
);

handleClickOutside = (event: Event) => {
// do not close when rename or delete branch modal is open
const modal = document.querySelector('.modal');
if (!modal && (!this.node || !this.node.contains(event.target as HTMLElement))) {
if (!this.node || !this.node.contains(event.target as HTMLElement)) {
this.props.onClose();
}
};
@@ -194,10 +192,8 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
menu.push(
<ComponentNavBranchesMenuItem
branch={branch}
canAdmin={this.props.canAdmin}
component={this.props.component}
key={branch.name}
onBranchesChange={this.props.onBranchesChange}
onSelect={this.handleSelect}
selected={branch.name === selected}
/>
@@ -208,10 +204,25 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
};

render() {
const { component } = this.props;
const showManageLink =
component.qualifier === 'TRK' &&
component.configuration &&
component.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: component.key } }}>
{translate('branches.manage')}
</Link>
</div>
)}
</div>
);
}

+ 26
- 103
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx View File

@@ -20,125 +20,48 @@
import * as React from 'react';
import { Link } from 'react-router';
import * as classNames from 'classnames';
import DeleteBranchModal from './DeleteBranchModal';
import RenameBranchModal from './RenameBranchModal';
import BranchStatus from '../../../../components/common/BranchStatus';
import { Branch, Component } from '../../../types';
import BranchIcon from '../../../../components/icons-components/BranchIcon';
import ChangeIcon from '../../../../components/icons-components/ChangeIcon';
import DeleteIcon from '../../../../components/icons-components/DeleteIcon';
import { isShortLivingBranch } from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
import { getProjectBranchUrl } from '../../../../helpers/urls';

export interface Props {
branch: Branch;
canAdmin?: boolean;
component: Component;
onBranchesChange: () => void;
onSelect: (branch: Branch) => void;
selected: boolean;
}

interface State {
deleteBranchModal: boolean;
renameBranchModal: boolean;
}

export default class ComponentNavBranchesMenuItem extends React.PureComponent<Props, State> {
state: State = { deleteBranchModal: false, renameBranchModal: false };

handleMouseEnter = () => {
this.props.onSelect(this.props.branch);
};

handleDeleteBranchClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ deleteBranchModal: true });
};

handleDeleteBranchClose = () => {
this.setState({ deleteBranchModal: false });
};

handleBranchDelete = () => {
this.props.onBranchesChange();
this.setState({ deleteBranchModal: false });
};

handleRenameBranchClick = (event: React.SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.setState({ renameBranchModal: true });
export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) {
const handleMouseEnter = () => {
props.onSelect(branch);
};

handleRenameBranchClose = () => {
this.setState({ renameBranchModal: false });
};

handleBranchRename = () => {
this.props.onBranchesChange();
this.setState({ renameBranchModal: false });
};

render() {
const { branch } = this.props;
return (
<li key={branch.name} onMouseEnter={this.handleMouseEnter}>
<Link
className={classNames('navbar-context-meta-branch-menu-item', {
active: this.props.selected
})}
to={getProjectBranchUrl(this.props.component.key, branch)}>
<div className="navbar-context-meta-branch-menu-item-name">
<BranchIcon
branch={branch}
className={classNames('little-spacer-right', {
'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
})}
/>
{branch.name}
{branch.isMain && (
<div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
)}
</div>
<div className="big-spacer-left note">
<BranchStatus branch={branch} concise={true} />
</div>
{this.props.canAdmin && (
<div className="navbar-context-meta-branch-menu-item-actions">
{branch.isMain ? (
<button className="js-rename button-link" onClick={this.handleRenameBranchClick}>
<ChangeIcon />
</button>
) : (
<button className="js-delete button-link" onClick={this.handleDeleteBranchClick}>
<DeleteIcon />
</button>
)}
</div>
)}
</Link>

{this.state.deleteBranchModal && (
<DeleteBranchModal
return (
<li key={branch.name} onMouseEnter={handleMouseEnter}>
<Link
className={classNames('navbar-context-meta-branch-menu-item', {
active: props.selected
})}
to={getProjectBranchUrl(props.component.key, branch)}>
<div className="navbar-context-meta-branch-menu-item-name">
<BranchIcon
branch={branch}
component={this.props.component.key}
onClose={this.handleDeleteBranchClose}
onDelete={this.handleBranchDelete}
className={classNames('little-spacer-right', {
'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan
})}
/>
)}

{this.state.renameBranchModal && (
<RenameBranchModal
branch={branch}
component={this.props.component.key}
onClose={this.handleRenameBranchClose}
onRename={this.handleBranchRename}
/>
)}
</li>
);
}
{branch.name}
{branch.isMain && (
<div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
)}
</div>
<div className="big-spacer-left note">
<BranchStatus branch={branch} concise={true} />
</div>
</Link>
</li>
);
}

+ 21
- 0
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

@@ -241,6 +241,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
renderAdministrationLinks() {
return [
this.renderSettingsLink(),
this.renderBranchesLink(),
this.renderProfilesLink(),
this.renderQualityGateLink(),
this.renderCustomMeasuresLink(),
@@ -274,6 +275,26 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
);
}

renderBranchesLink() {
if (
!this.context.branchesEnabled ||
!this.isProject() ||
!this.getConfiguration().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.getConfiguration().showQualityProfiles) {
return null;

+ 2
- 4
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNav-test.tsx View File

@@ -62,15 +62,13 @@ const component = {

it('loads status', () => {
getTasksForComponent.mockClear();
mount(
<ComponentNav branches={[]} component={component} location={{}} onBranchesChange={jest.fn()} />
);
mount(<ComponentNav branches={[]} component={component} location={{}} />);
expect(getTasksForComponent).toBeCalledWith('component');
});

it('renders', () => {
const wrapper = shallow(
<ComponentNav branches={[]} component={component} location={{}} onBranchesChange={jest.fn()} />
<ComponentNav branches={[]} component={component} location={{}} />
);
wrapper.setState({
incremental: true,

+ 1
- 10
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx View File

@@ -40,7 +40,6 @@ it('renders main branch', () => {
branches={[branch, fooBranch]}
component={component}
currentBranch={branch}
onBranchesChange={jest.fn()}
/>,
{ context: { branchesEnabled: true } }
)
@@ -62,7 +61,6 @@ it('renders short-living branch', () => {
branches={[branch, fooBranch]}
component={component}
currentBranch={branch}
onBranchesChange={jest.fn()}
/>,
{ context: { branchesEnabled: true } }
)
@@ -77,7 +75,6 @@ it('opens menu', () => {
branches={[branch, fooBranch]}
component={component}
currentBranch={branch}
onBranchesChange={jest.fn()}
/>,
{ context: { branchesEnabled: true } }
);
@@ -90,12 +87,7 @@ it('renders single branch popup', () => {
const branch: MainBranch = { isMain: true, name: 'master' };
const component = {} as Component;
const wrapper = shallow(
<ComponentNavBranch
branches={[branch]}
component={component}
currentBranch={branch}
onBranchesChange={jest.fn()}
/>,
<ComponentNavBranch branches={[branch]} component={component} currentBranch={branch} />,
{ context: { branchesEnabled: true } }
);
expect(wrapper).toMatchSnapshot();
@@ -112,7 +104,6 @@ it('renders nothing when no branch support', () => {
branches={[branch, fooBranch]}
component={component}
currentBranch={branch}
onBranchesChange={jest.fn()}
/>,
{ context: { branchesEnabled: false } }
);

+ 4
- 7
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx View File

@@ -29,16 +29,15 @@ import {
} from '../../../../types';
import { elementKeydown } from '../../../../../helpers/testUtils';

const project = { key: 'component' } as Component;
const component = { key: 'component' } as Component;

it('renders list', () => {
expect(
shallow(
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), longBranch('bar'), shortBranch('baz', true)]}
component={project}
component={component}
currentBranch={mainBranch()}
onBranchesChange={jest.fn()}
onClose={jest.fn()}
/>
)
@@ -49,9 +48,8 @@ it('searches', () => {
const wrapper = shallow(
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
component={project}
component={component}
currentBranch={mainBranch()}
onBranchesChange={jest.fn()}
onClose={jest.fn()}
/>
);
@@ -63,9 +61,8 @@ it('selects next & previous', () => {
const wrapper = shallow(
<ComponentNavBranchesMenu
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]}
component={project}
component={component}
currentBranch={mainBranch()}
onBranchesChange={jest.fn()}
onClose={jest.fn()}
/>
);

+ 0
- 20
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx View File

@@ -21,7 +21,6 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavBranchesMenuItem, { Props } from '../ComponentNavBranchesMenuItem';
import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../../types';
import { click } from '../../../../../helpers/testUtils';

const component = { key: 'component' } as Component;

@@ -47,30 +46,11 @@ it('renders short-living orhpan branch', () => {
expect(shallowRender({ branch: { ...shortBranch, isOrphan: true } })).toMatchSnapshot();
});

it('renames main branch', () => {
const onBranchesChange = jest.fn();
const wrapper = shallowRender({ branch: mainBranch, canAdmin: true, onBranchesChange });

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

it('deletes short-living branch', () => {
const onBranchesChange = jest.fn();
const wrapper = shallowRender({ canAdmin: true, onBranchesChange });

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

function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
return shallow(
<ComponentNavBranchesMenuItem
branch={shortBranch}
component={component}
onBranchesChange={jest.fn()}
onSelect={jest.fn()}
selected={false}
{...props}

+ 0
- 7
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap View File

@@ -39,7 +39,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
onBranchesChange={[Function]}
onSelect={[Function]}
selected={true}
/>
@@ -79,7 +78,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
@@ -103,7 +101,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
@@ -123,7 +120,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
@@ -163,7 +159,6 @@ exports[`renders list 1`] = `
"key": "component",
}
}
onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>
@@ -218,7 +213,6 @@ exports[`searches 1`] = `
"key": "component",
}
}
onBranchesChange={[Function]}
onSelect={[Function]}
selected={true}
/>
@@ -238,7 +232,6 @@ exports[`searches 1`] = `
"key": "component",
}
}
onBranchesChange={[Function]}
onSelect={[Function]}
selected={false}
/>

+ 51
- 0
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap View File

@@ -129,6 +129,23 @@ exports[`should work for all qualifiers 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"
@@ -1011,6 +1028,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"
@@ -1216,6 +1250,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"

+ 2
- 0
server/sonar-web/src/main/js/app/utils/startReactApp.js View File

@@ -55,6 +55,7 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes';
import portfolioRoutes from '../../apps/portfolio/routes';
import projectActivityRoutes from '../../apps/projectActivity/routes';
import projectAdminRoutes from '../../apps/project-admin/routes';
import projectBranchesRoutes from '../../apps/projectBranches/routes';
import projectQualityGateRoutes from '../../apps/projectQualityGate/routes';
import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes';
import projectsRoutes from '../../apps/projects/routes';
@@ -206,6 +207,7 @@ const startReactApp = () => {
component={ProjectAdminPageExtension}
/>
<Route path="project/background_tasks" childRoutes={backgroundTasksRoutes} />
<Route path="project/branches" childRoutes={projectBranchesRoutes} />
<Route path="project/settings" childRoutes={settingsRoutes} />
<Route path="project_roles" childRoutes={projectPermissionsRoutes} />
</Route>

+ 60
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/App.tsx View File

@@ -0,0 +1,60 @@
/*
* 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>
);
}

+ 139
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/BranchRow.tsx View File

@@ -0,0 +1,139 @@
/*
* 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}
{branch.isMain && (
<div className="outline-badge spacer-left">{translate('branches.main_branch')}</div>
)}
</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>
);
}
}

server/sonar-web/src/main/js/app/components/nav/component/DeleteBranchModal.tsx → server/sonar-web/src/main/js/apps/projectBranches/components/DeleteBranchModal.tsx View File

@@ -19,9 +19,9 @@
*/
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';
import { deleteBranch } from '../../../api/branches';
import { Branch } from '../../../app/types';
import { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
branch: Branch;
@@ -66,7 +66,6 @@ export default class DeleteBranchModal extends React.PureComponent<Props, State>

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


server/sonar-web/src/main/js/app/components/nav/component/RenameBranchModal.tsx → server/sonar-web/src/main/js/apps/projectBranches/components/RenameBranchModal.tsx View File

@@ -19,9 +19,9 @@
*/
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';
import { renameBranch } from '../../../api/branches';
import { Branch } from '../../../app/types';
import { translate } from '../../../helpers/l10n';

interface Props {
branch: Branch;
@@ -70,7 +70,6 @@ export default class RenameBranchModal extends React.PureComponent<Props, State>

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


+ 34
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/App-test.tsx View File

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

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

server/sonar-web/src/main/js/app/components/nav/component/__tests__/DeleteBranchModal-test.tsx → server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/DeleteBranchModal-test.tsx View File

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

server/sonar-web/src/main/js/app/components/nav/component/__tests__/RenameBranchModal-test.tsx → server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/RenameBranchModal-test.tsx View File

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

+ 73
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/App-test.tsx.snap View File

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

+ 100
- 0
server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchRow-test.tsx.snap View File

@@ -0,0 +1,100 @@
// 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
<div
className="outline-badge spacer-left"
>
branches.main_branch
</div>
</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>
`;

server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap → server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/DeleteBranchModal-test.tsx.snap View File


server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap → server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/RenameBranchModal-test.tsx.snap View File


+ 30
- 0
server/sonar-web/src/main/js/apps/projectBranches/routes.ts View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/App.js View File

@@ -98,7 +98,7 @@ export default class App extends React.PureComponent {
link: (
<Link
to={{
pathname: '/project/settings',
pathname: '/project/branches',
query: { id: this.props.component && this.props.component.key }
}}>
{translate('branches.settings_hint_tab')}

+ 3
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -3178,13 +3178,14 @@ branches.no_support.header.text=Analyze each branch of your project separately w
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
branches.orphan_branch=Orphan Branch
branches.orphan_branches=Orphan Branches
branches.orphan_branches.tooltip=When a target branch of a short-living branch was deleted, this short-living branch becomes orphan.
branches.main_branch=Main Branch
branches.branch_settings=Branch Settings
branches.settings_hint=To administrate your project, you have to go to your main branch's {link} tab.
branches.settings_hint_tab=Administration
branches.settings_hint=To administrate your branches, you have to go to your main branch's {link} tab.
branches.settings_hint_tab=Administration > Branches


#------------------------------------------------------------------------------

Loading…
Cancel
Save