@@ -122,36 +122,36 @@ export default class ComponentContainer extends React.PureComponent<Props, State | |||
const { query } = this.props.location; | |||
const { branches, component, loading } = this.state; | |||
if (loading) { | |||
return <i className="spinner" />; | |||
} | |||
if (!component) { | |||
if (!loading && !component) { | |||
return <ComponentContainerNotFound />; | |||
} | |||
const branch = branches.find(b => (query.branch ? b.name === query.branch : b.isMain)); | |||
const isFile = ['FIL', 'UTS'].includes(component.qualifier); | |||
const configuration = component.configuration || {}; | |||
return ( | |||
<div> | |||
{!isFile && ( | |||
{component && | |||
!['FIL', 'UTS'].includes(component.qualifier) && ( | |||
<ComponentNav | |||
branches={branches} | |||
currentBranch={branch} | |||
component={component} | |||
conf={configuration} | |||
location={this.props.location} | |||
onBranchesChange={this.handleBranchesChange} | |||
/> | |||
)} | |||
{React.cloneElement(this.props.children, { | |||
branch, | |||
branches, | |||
component: component, | |||
onBranchesChange: this.handleBranchesChange, | |||
onComponentChange: this.handleComponentChange | |||
})} | |||
{loading ? ( | |||
<div className="page page-limited"> | |||
<i className="spinner" /> | |||
</div> | |||
) : ( | |||
React.cloneElement(this.props.children, { | |||
branch, | |||
branches, | |||
component, | |||
onComponentChange: this.handleComponentChange | |||
}) | |||
)} | |||
</div> | |||
); | |||
} |
@@ -106,20 +106,3 @@ 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'); | |||
}); |
@@ -25,4 +25,14 @@ | |||
.navbar-context-meta-branch-menu-item { | |||
display: flex !important; | |||
justify-content: space-between; | |||
align-items: center; | |||
} | |||
.navbar-context-meta-branch-menu-item-name { | |||
flex: 1; | |||
} | |||
.navbar-context-meta-branch-menu-item-actions { | |||
height: 12px; | |||
margin-left: 32px; | |||
} |
@@ -24,7 +24,7 @@ import ComponentNavMeta from './ComponentNavMeta'; | |||
import ComponentNavMenu from './ComponentNavMenu'; | |||
import ComponentNavBranch from './ComponentNavBranch'; | |||
import RecentHistory from '../../RecentHistory'; | |||
import { Branch, Component, ComponentConfiguration } from '../../../types'; | |||
import { Branch, Component } from '../../../types'; | |||
import ContextNavBar from '../../../../components/nav/ContextNavBar'; | |||
import { getTasksForComponent } from '../../../../api/ce'; | |||
import { STATUSES } from '../../../../apps/background-tasks/constants'; | |||
@@ -34,8 +34,8 @@ interface Props { | |||
branches: Branch[]; | |||
currentBranch?: Branch; | |||
component: Component; | |||
conf: ComponentConfiguration; | |||
location: {}; | |||
onBranchesChange: () => void; | |||
} | |||
interface State { | |||
@@ -102,17 +102,17 @@ export default class ComponentNav extends React.PureComponent<Props, State> { | |||
{this.props.currentBranch && ( | |||
<ComponentNavBranch | |||
branches={this.props.branches} | |||
component={this.props.component} | |||
currentBranch={this.props.currentBranch} | |||
// to close dropdown on any location change | |||
location={this.props.location} | |||
project={this.props.component} | |||
onBranchesChange={this.props.onBranchesChange} | |||
/> | |||
)} | |||
<ComponentNavMeta | |||
branch={this.props.currentBranch} | |||
component={this.props.component} | |||
conf={this.props.conf} | |||
incremental={this.state.incremental} | |||
isInProgress={this.state.isInProgress} | |||
isFailed={this.state.isFailed} | |||
@@ -122,7 +122,6 @@ export default class ComponentNav extends React.PureComponent<Props, State> { | |||
<ComponentNavMenu | |||
branch={this.props.currentBranch} | |||
component={this.props.component} | |||
conf={this.props.conf} | |||
// to re-render selected menu item | |||
location={this.props.location} | |||
/> |
@@ -22,7 +22,6 @@ import * as classNames from 'classnames'; | |||
import * as PropTypes from 'prop-types'; | |||
import ComponentNavBranchesMenu from './ComponentNavBranchesMenu'; | |||
import SingleBranchHelperPopup from './SingleBranchHelperPopup'; | |||
import NoBranchSupportPopup from './NoBranchSupportPopup'; | |||
import { Branch, Component } from '../../../types'; | |||
import BranchIcon from '../../../../components/icons-components/BranchIcon'; | |||
import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
@@ -33,9 +32,10 @@ import Tooltip from '../../../../components/controls/Tooltip'; | |||
interface Props { | |||
branches: Branch[]; | |||
component: Component; | |||
currentBranch: Branch; | |||
location?: any; | |||
project: Component; | |||
onBranchesChange: () => void; | |||
} | |||
interface State { | |||
@@ -53,8 +53,7 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
}; | |||
static contextTypes = { | |||
branchesEnabled: PropTypes.bool.isRequired, | |||
onSonarCloud: PropTypes.bool | |||
branchesEnabled: PropTypes.bool.isRequired | |||
}; | |||
componentDidMount() { | |||
@@ -63,8 +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.component !== this.props.component || | |||
this.differentBranches(nextProps.currentBranch, this.props.currentBranch) || | |||
nextProps.location !== this.props.location | |||
) { | |||
this.setState({ dropdownOpen: false, singleBranchPopupOpen: false }); | |||
@@ -75,11 +74,16 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
this.mounted = false; | |||
} | |||
differentBranches(a: Branch, b: Branch) { | |||
// if main branch changes name, we should not close the dropdown | |||
return a.isMain && b.isMain ? false : a.name !== b.name; | |||
} | |||
handleClick = (event: React.SyntheticEvent<HTMLElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.currentTarget.blur(); | |||
this.setState({ dropdownOpen: true }); | |||
this.setState(state => ({ dropdownOpen: !state.dropdownOpen })); | |||
}; | |||
closeDropdown = () => { | |||
@@ -117,12 +121,15 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
}; | |||
renderDropdown = () => { | |||
const { configuration } = this.props.component; | |||
return this.state.dropdownOpen ? ( | |||
<ComponentNavBranchesMenu | |||
branches={this.props.branches} | |||
canAdmin={configuration && configuration.showSettings} | |||
component={this.props.component} | |||
currentBranch={this.props.currentBranch} | |||
onBranchesChange={this.props.onBranchesChange} | |||
onClose={this.closeDropdown} | |||
project={this.props.project} | |||
/> | |||
) : null; | |||
}; | |||
@@ -160,35 +167,11 @@ export default class ComponentNavBranch extends React.PureComponent<Props, State | |||
</div> | |||
); | |||
renderNoBranchSupportPopup = () => ( | |||
<div className="display-inline-block spacer-left"> | |||
<a className="link-no-underline" href="#" onClick={this.handleNoBranchSupportClick}> | |||
<HelpIcon fill="#cdcdcd" /> | |||
</a> | |||
<BubblePopupHelper | |||
isOpen={this.state.noBranchSupportPopupOpen} | |||
position="bottomleft" | |||
popup={<NoBranchSupportPopup />} | |||
togglePopup={this.toggleNoBranchSupportPopup} | |||
/> | |||
</div> | |||
); | |||
render() { | |||
const { branches, currentBranch } = this.props; | |||
if (this.context.onSonarCloud && !this.context.branchesEnabled) { | |||
return null; | |||
} | |||
if (!this.context.branchesEnabled) { | |||
return ( | |||
<div className="navbar-context-branches"> | |||
<BranchIcon branch={currentBranch} className="little-spacer-right" color="#cdcdcd" /> | |||
<span className="note">{currentBranch.name}</span> | |||
{this.renderNoBranchSupportPopup()} | |||
</div> | |||
); | |||
return null; | |||
} | |||
if (branches.length < 2) { |
@@ -28,14 +28,15 @@ import { | |||
} from '../../../../helpers/branches'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
import { getProjectBranchUrl } from '../../../../helpers/urls'; | |||
import { Link } from 'react-router'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
interface Props { | |||
branches: Branch[]; | |||
canAdmin?: boolean; | |||
component: Component; | |||
currentBranch: Branch; | |||
onBranchesChange: () => void; | |||
onClose: () => void; | |||
project: Component; | |||
} | |||
interface State { | |||
@@ -65,7 +66,9 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
); | |||
handleClickOutside = (event: Event) => { | |||
if (!this.node || !this.node.contains(event.target as HTMLElement)) { | |||
// 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))) { | |||
this.props.onClose(); | |||
} | |||
}; | |||
@@ -125,11 +128,23 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
}; | |||
getSelected = () => { | |||
if (this.state.selected) { | |||
return this.state.selected; | |||
} | |||
const branches = this.getFilteredBranches(); | |||
return this.state.selected || (branches.length > 0 && branches[0].name); | |||
if (branches.find(b => b.name === this.props.currentBranch.name)) { | |||
return this.props.currentBranch.name; | |||
} | |||
if (branches.length > 0) { | |||
return branches[0].name; | |||
} | |||
return undefined; | |||
}; | |||
getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.project.key, branch); | |||
getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.component.key, branch); | |||
isSelected = (branch: Branch) => branch.name === this.getSelected(); | |||
@@ -179,8 +194,10 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
menu.push( | |||
<ComponentNavBranchesMenuItem | |||
branch={branch} | |||
component={this.props.project} | |||
canAdmin={this.props.canAdmin} | |||
component={this.props.component} | |||
key={branch.name} | |||
onBranchesChange={this.props.onBranchesChange} | |||
onSelect={this.handleSelect} | |||
selected={branch.name === selected} | |||
/> | |||
@@ -191,23 +208,10 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props, | |||
}; | |||
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> | |||
); | |||
} |
@@ -20,48 +20,125 @@ | |||
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'; | |||
interface Props { | |||
export interface Props { | |||
branch: Branch; | |||
canAdmin?: boolean; | |||
component: Component; | |||
onBranchesChange: () => void; | |||
onSelect: (branch: Branch) => void; | |||
selected: boolean; | |||
} | |||
export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) { | |||
const handleMouseEnter = () => { | |||
props.onSelect(branch); | |||
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 }); | |||
}; | |||
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> | |||
<BranchIcon | |||
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 | |||
branch={branch} | |||
className={classNames('little-spacer-right', { | |||
'big-spacer-left': isShortLivingBranch(branch) && !branch.isOrphan | |||
})} | |||
component={this.props.component.key} | |||
onClose={this.handleDeleteBranchClose} | |||
onDelete={this.handleBranchDelete} | |||
/> | |||
{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> | |||
); | |||
)} | |||
{this.state.renameBranchModal && ( | |||
<RenameBranchModal | |||
branch={branch} | |||
component={this.props.component.key} | |||
onClose={this.handleRenameBranchClose} | |||
onRename={this.handleBranchRename} | |||
/> | |||
)} | |||
</li> | |||
); | |||
} | |||
} |
@@ -21,7 +21,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 { Branch, Component, ComponentExtension } from '../../../types'; | |||
import NavBarTabs from '../../../../components/nav/NavBarTabs'; | |||
import { | |||
isShortLivingBranch, | |||
@@ -48,7 +48,6 @@ const SETTINGS_URLS = [ | |||
interface Props { | |||
branch?: Branch; | |||
component: Component; | |||
conf: ComponentConfiguration; | |||
location?: any; | |||
} | |||
@@ -74,6 +73,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
return this.props.component.qualifier === 'APP'; | |||
} | |||
getConfiguration() { | |||
return this.props.component.configuration || {}; | |||
} | |||
renderDashboardLink() { | |||
if (this.props.branch && isShortLivingBranch(this.props.branch)) { | |||
return null; | |||
@@ -193,7 +196,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
renderAdministration() { | |||
const { branch } = this.props; | |||
if (!this.props.conf.showSettings || (branch && isShortLivingBranch(branch))) { | |||
if (!this.getConfiguration().showSettings || (branch && isShortLivingBranch(branch))) { | |||
return null; | |||
} | |||
@@ -209,7 +212,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
pathname: '/project/settings', | |||
query: { branch: getBranchName(branch), id: this.props.component.key } | |||
}}> | |||
{translate('layout.settings')} | |||
{translate('branches.branch_settings')} | |||
</Link> | |||
</li> | |||
); | |||
@@ -238,7 +241,6 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
renderAdministrationLinks() { | |||
return [ | |||
this.renderSettingsLink(), | |||
this.renderBranchesLink(), | |||
this.renderProfilesLink(), | |||
this.renderQualityGateLink(), | |||
this.renderCustomMeasuresLink(), | |||
@@ -252,7 +254,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderSettingsLink() { | |||
if (!this.props.conf.showSettings || this.isApplication() || this.isPortfolio()) { | |||
if (!this.getConfiguration().showSettings || this.isApplication() || this.isPortfolio()) { | |||
return null; | |||
} | |||
return ( | |||
@@ -272,23 +274,8 @@ 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) { | |||
if (!this.getConfiguration().showQualityProfiles) { | |||
return null; | |||
} | |||
return ( | |||
@@ -303,7 +290,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderQualityGateLink() { | |||
if (!this.props.conf.showQualityGates) { | |||
if (!this.getConfiguration().showQualityGates) { | |||
return null; | |||
} | |||
return ( | |||
@@ -318,7 +305,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderCustomMeasuresLink() { | |||
if (!this.props.conf.showManualMeasures) { | |||
if (!this.getConfiguration().showManualMeasures) { | |||
return null; | |||
} | |||
return ( | |||
@@ -333,7 +320,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderLinksLink() { | |||
if (!this.props.conf.showLinks) { | |||
if (!this.getConfiguration().showLinks) { | |||
return null; | |||
} | |||
return ( | |||
@@ -348,7 +335,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderPermissionsLink() { | |||
if (!this.props.conf.showPermissions) { | |||
if (!this.getConfiguration().showPermissions) { | |||
return null; | |||
} | |||
return ( | |||
@@ -363,7 +350,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderBackgroundTasksLink() { | |||
if (!this.props.conf.showBackgroundTasks) { | |||
if (!this.getConfiguration().showBackgroundTasks) { | |||
return null; | |||
} | |||
return ( | |||
@@ -378,7 +365,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
} | |||
renderUpdateKeyLink() { | |||
if (!this.props.conf.showUpdateKey) { | |||
if (!this.getConfiguration().showUpdateKey) { | |||
return null; | |||
} | |||
return ( | |||
@@ -395,7 +382,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
renderDeletionLink() { | |||
const { qualifier } = this.props.component; | |||
if (!this.props.conf.showSettings) { | |||
if (!this.getConfiguration().showSettings) { | |||
return null; | |||
} | |||
@@ -426,7 +413,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
}; | |||
renderAdminExtensions() { | |||
const extensions = this.props.conf.extensions || []; | |||
const extensions = this.getConfiguration().extensions || []; | |||
return extensions.map(e => this.renderExtension(e, true)); | |||
} | |||
@@ -446,9 +433,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> { | |||
{translate('more')} | |||
<i className="icon-dropdown" /> | |||
</a> | |||
<ul className="dropdown-menu"> | |||
{extensions.map(e => this.renderExtension(e, false))} | |||
</ul> | |||
<ul className="dropdown-menu">{extensions.map(e => this.renderExtension(e, false))}</ul> | |||
</li> | |||
); | |||
} |
@@ -20,7 +20,7 @@ | |||
import * as React from 'react'; | |||
import IncrementalBadge from './IncrementalBadge'; | |||
import BranchStatus from '../../../../components/common/BranchStatus'; | |||
import { Branch, Component, ComponentConfiguration } from '../../../types'; | |||
import { Branch, Component } from '../../../types'; | |||
import Tooltip from '../../../../components/controls/Tooltip'; | |||
import PendingIcon from '../../../../components/icons-components/PendingIcon'; | |||
import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter'; | |||
@@ -30,7 +30,6 @@ import { isShortLivingBranch } from '../../../../helpers/branches'; | |||
interface Props { | |||
branch?: Branch; | |||
component: Component; | |||
conf: ComponentConfiguration; | |||
incremental?: boolean; | |||
isInProgress?: boolean; | |||
isFailed?: boolean; | |||
@@ -39,7 +38,8 @@ interface Props { | |||
export default function ComponentNavMeta(props: Props) { | |||
const metaList = []; | |||
const canSeeBackgroundTasks = props.conf.showBackgroundTasks; | |||
const canSeeBackgroundTasks = | |||
props.component.configuration != undefined && props.component.configuration.showBackgroundTasks; | |||
const backgroundTasksUrl = | |||
(window as any).baseUrl + | |||
`/project/background_tasks?id=${encodeURIComponent(props.component.key)}`; |
@@ -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,6 +66,7 @@ export default class DeleteBranchModal extends React.PureComponent<Props, State> | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.props.onClose(); | |||
}; | |||
@@ -1,48 +0,0 @@ | |||
/* | |||
* 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 BubblePopup from '../../../../components/common/BubblePopup'; | |||
import { translate } from '../../../../helpers/l10n'; | |||
interface Props { | |||
popupPosition?: any; | |||
} | |||
export default function NoBranchSupportPopup(props: Props) { | |||
return ( | |||
<BubblePopup position={props.popupPosition} customClass="bubble-popup-bottom"> | |||
<div className="abs-width-400"> | |||
<h6 className="spacer-bottom">{translate('branches.no_support.header')}</h6> | |||
<p className="big-spacer-bottom markdown">{translate('branches.no_support.header.text')}</p> | |||
<p> | |||
<a href="https://redirect.sonarsource.com/doc/branches.html" target="_blank"> | |||
{translate('learn_more')} | |||
</a> | |||
<a | |||
className="button spacer-left" | |||
href="https://www.sonarsource.com/company/contact/" | |||
target="_blank"> | |||
{translate('buy_developer_pack')} | |||
</a> | |||
</p> | |||
</div> | |||
</BubblePopup> | |||
); | |||
} |
@@ -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,6 +70,7 @@ export default class RenameBranchModal extends React.PureComponent<Props, State> | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
this.props.onClose(); | |||
}; | |||
@@ -62,13 +62,15 @@ const component = { | |||
it('loads status', () => { | |||
getTasksForComponent.mockClear(); | |||
mount(<ComponentNav branches={[]} component={component} conf={{}} location={{}} />); | |||
mount( | |||
<ComponentNav branches={[]} component={component} location={{}} onBranchesChange={jest.fn()} /> | |||
); | |||
expect(getTasksForComponent).toBeCalledWith('component'); | |||
}); | |||
it('renders', () => { | |||
const wrapper = shallow( | |||
<ComponentNav branches={[]} component={component} conf={{}} location={{}} /> | |||
<ComponentNav branches={[]} component={component} location={{}} onBranchesChange={jest.fn()} /> | |||
); | |||
wrapper.setState({ | |||
incremental: true, |
@@ -38,8 +38,9 @@ it('renders main branch', () => { | |||
shallow( | |||
<ComponentNavBranch | |||
branches={[branch, fooBranch]} | |||
component={component} | |||
currentBranch={branch} | |||
project={component} | |||
onBranchesChange={jest.fn()} | |||
/>, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
@@ -59,8 +60,9 @@ it('renders short-living branch', () => { | |||
shallow( | |||
<ComponentNavBranch | |||
branches={[branch, fooBranch]} | |||
component={component} | |||
currentBranch={branch} | |||
project={component} | |||
onBranchesChange={jest.fn()} | |||
/>, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
@@ -73,8 +75,9 @@ it('opens menu', () => { | |||
const wrapper = shallow( | |||
<ComponentNavBranch | |||
branches={[branch, fooBranch]} | |||
component={component} | |||
currentBranch={branch} | |||
project={component} | |||
onBranchesChange={jest.fn()} | |||
/>, | |||
{ context: { branchesEnabled: true } } | |||
); | |||
@@ -87,7 +90,12 @@ it('renders single branch popup', () => { | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranch branches={[branch]} currentBranch={branch} project={component} />, | |||
<ComponentNavBranch | |||
branches={[branch]} | |||
component={component} | |||
currentBranch={branch} | |||
onBranchesChange={jest.fn()} | |||
/>, | |||
{ context: { branchesEnabled: true } } | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
@@ -96,29 +104,17 @@ it('renders single branch popup', () => { | |||
expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(true); | |||
}); | |||
it('renders no branch support popup', () => { | |||
it('renders nothing when no branch support', () => { | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranch | |||
branches={[branch, fooBranch]} | |||
component={component} | |||
currentBranch={branch} | |||
project={component} | |||
onBranchesChange={jest.fn()} | |||
/>, | |||
{ context: { branchesEnabled: false } } | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(false); | |||
click(wrapper.find('a')); | |||
expect(wrapper.find('BubblePopupHelper').prop('isOpen')).toBe(true); | |||
}); | |||
it('renders nothing on SonarCloud without branch support', () => { | |||
const branch: MainBranch = { isMain: true, name: 'master' }; | |||
const component = {} as Component; | |||
const wrapper = shallow( | |||
<ComponentNavBranch branches={[branch]} currentBranch={branch} project={component} />, | |||
{ context: { branchesEnabled: false, onSonarCloud: true } } | |||
); | |||
expect(wrapper.type()).toBeNull(); | |||
}); |
@@ -36,9 +36,10 @@ it('renders list', () => { | |||
shallow( | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), longBranch('bar'), shortBranch('baz', true)]} | |||
component={project} | |||
currentBranch={mainBranch()} | |||
onBranchesChange={jest.fn()} | |||
onClose={jest.fn()} | |||
project={project} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
@@ -48,9 +49,10 @@ it('searches', () => { | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} | |||
component={project} | |||
currentBranch={mainBranch()} | |||
onBranchesChange={jest.fn()} | |||
onClose={jest.fn()} | |||
project={project} | |||
/> | |||
); | |||
wrapper.setState({ query: 'bar' }); | |||
@@ -61,9 +63,10 @@ it('selects next & previous', () => { | |||
const wrapper = shallow( | |||
<ComponentNavBranchesMenu | |||
branches={[mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')]} | |||
component={project} | |||
currentBranch={mainBranch()} | |||
onBranchesChange={jest.fn()} | |||
onClose={jest.fn()} | |||
project={project} | |||
/> | |||
); | |||
elementKeydown(wrapper.find('input'), 40); |
@@ -19,63 +19,61 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavBranchesMenuItem from '../ComponentNavBranchesMenuItem'; | |||
import ComponentNavBranchesMenuItem, { Props } from '../ComponentNavBranchesMenuItem'; | |||
import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../../types'; | |||
import { click } from '../../../../../helpers/testUtils'; | |||
const component = { key: 'component' } as Component; | |||
const shortBranch: ShortLivingBranch = { | |||
isMain: false, | |||
mergeBranch: 'master', | |||
name: 'foo', | |||
status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT | |||
}; | |||
const mainBranch: MainBranch = { isMain: true, name: 'master' }; | |||
it('renders main branch', () => { | |||
const component = { key: 'component' } as Component; | |||
const mainBranch: MainBranch = { isMain: true, name: 'master' }; | |||
expect( | |||
shallow( | |||
<ComponentNavBranchesMenuItem | |||
branch={mainBranch} | |||
component={component} | |||
onSelect={jest.fn()} | |||
selected={false} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect(shallowRender({ branch: mainBranch })).toMatchSnapshot(); | |||
}); | |||
it('renders short-living branch', () => { | |||
const component = { key: 'component' } as Component; | |||
const shortBranch: ShortLivingBranch = { | |||
isMain: false, | |||
mergeBranch: 'master', | |||
name: 'foo', | |||
status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT | |||
}; | |||
expect( | |||
shallow( | |||
<ComponentNavBranchesMenuItem | |||
branch={shortBranch} | |||
component={component} | |||
onSelect={jest.fn()} | |||
selected={false} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('renders short-living orhpan branch', () => { | |||
const component = { key: 'component' } as Component; | |||
const shortBranch: ShortLivingBranch = { | |||
isMain: false, | |||
isOrphan: true, | |||
mergeBranch: 'master', | |||
name: 'foo', | |||
status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT | |||
}; | |||
expect( | |||
shallow( | |||
<ComponentNavBranchesMenuItem | |||
branch={shortBranch} | |||
component={component} | |||
onSelect={jest.fn()} | |||
selected={false} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
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} | |||
/> | |||
); | |||
} |
@@ -20,55 +20,47 @@ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import ComponentNavMenu from '../ComponentNavMenu'; | |||
import { | |||
Component, | |||
ShortLivingBranch, | |||
BranchType, | |||
LongLivingBranch, | |||
MainBranch | |||
} from '../../../../types'; | |||
import { ShortLivingBranch, BranchType, LongLivingBranch, MainBranch } from '../../../../types'; | |||
const mainBranch: MainBranch = { | |||
isMain: true, | |||
name: 'master' | |||
const mainBranch: MainBranch = { isMain: true, name: 'master' }; | |||
const baseComponent = { | |||
breadcrumbs: [], | |||
key: 'foo', | |||
name: 'foo', | |||
organization: 'org', | |||
qualifier: 'TRK' | |||
}; | |||
it('should work with extensions', () => { | |||
const component = { | |||
key: 'foo', | |||
qualifier: 'TRK', | |||
...baseComponent, | |||
configuration: { showSettings: true, extensions: [{ key: 'foo', name: 'Foo' }] }, | |||
extensions: [{ key: 'component-foo', name: 'ComponentFoo' }] | |||
}; | |||
const conf = { | |||
showSettings: true, | |||
extensions: [{ key: 'foo', name: 'Foo' }] | |||
}; | |||
expect( | |||
shallow( | |||
<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
shallow(<ComponentNavMenu branch={mainBranch} component={component} />, { | |||
context: { branchesEnabled: true } | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should work with multiple extensions', () => { | |||
const component = { | |||
key: 'foo', | |||
qualifier: 'TRK', | |||
...baseComponent, | |||
configuration: { | |||
showSettings: true, | |||
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }] | |||
}, | |||
extensions: [ | |||
{ key: 'component-foo', name: 'ComponentFoo' }, | |||
{ key: 'component-bar', name: 'ComponentBar' } | |||
] | |||
}; | |||
const conf = { | |||
showSettings: true, | |||
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }] | |||
}; | |||
expect( | |||
shallow( | |||
<ComponentNavMenu branch={mainBranch} component={component as Component} conf={conf} />, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
shallow(<ComponentNavMenu branch={mainBranch} component={component} />, { | |||
context: { branchesEnabled: true } | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -79,10 +71,9 @@ it('should work for short-living branches', () => { | |||
name: 'feature', | |||
type: BranchType.SHORT | |||
}; | |||
const component = { key: 'foo', qualifier: 'TRK' } as Component; | |||
const conf = { showSettings: true }; | |||
const component = { ...baseComponent, configuration: { showSettings: true } }; | |||
expect( | |||
shallow(<ComponentNavMenu branch={branch} component={component} conf={conf} />, { | |||
shallow(<ComponentNavMenu branch={branch} component={component} />, { | |||
context: { branchesEnabled: true } | |||
}) | |||
).toMatchSnapshot(); | |||
@@ -90,12 +81,15 @@ it('should work for short-living branches', () => { | |||
it('should work for long-living branches', () => { | |||
const branch: LongLivingBranch = { isMain: false, name: 'release', type: BranchType.LONG }; | |||
const component = { key: 'foo', qualifier: 'TRK' } as Component; | |||
[true, false].forEach(showSettings => | |||
expect( | |||
shallow(<ComponentNavMenu branch={branch} component={component} conf={{ showSettings }} />, { | |||
context: { branchesEnabled: true } | |||
}) | |||
shallow( | |||
<ComponentNavMenu | |||
branch={branch} | |||
component={{ ...baseComponent, configuration: { showSettings } }} | |||
/>, | |||
{ context: { branchesEnabled: true } } | |||
) | |||
).toMatchSnapshot() | |||
); | |||
}); | |||
@@ -103,20 +97,12 @@ it('should work for long-living branches', () => { | |||
it('should work for all qualifiers', () => { | |||
['TRK', 'BRC', 'VW', 'SVW', 'APP'].forEach(checkWithQualifier); | |||
expect.assertions(5); | |||
function checkWithQualifier(qualifier: string) { | |||
const component = { key: 'foo', qualifier } as Component; | |||
const component = { ...baseComponent, configuration: { showSettings: true }, qualifier }; | |||
expect( | |||
shallow( | |||
<ComponentNavMenu | |||
branch={mainBranch} | |||
component={component} | |||
conf={{ showSettings: true }} | |||
/>, | |||
{ | |||
context: { branchesEnabled: true } | |||
} | |||
) | |||
shallow(<ComponentNavMenu branch={mainBranch} component={component} />, { | |||
context: { branchesEnabled: true } | |||
}) | |||
).toMatchSnapshot(); | |||
} | |||
}); |
@@ -39,12 +39,7 @@ it('renders incremental badge', () => { | |||
function check(incremental: boolean) { | |||
expect( | |||
shallow( | |||
<ComponentNavMeta | |||
branch={{} as Branch} | |||
component={component} | |||
conf={{}} | |||
incremental={incremental} | |||
/> | |||
<ComponentNavMeta branch={{} as Branch} component={component} incremental={incremental} /> | |||
).find('IncrementalBadge') | |||
).toHaveLength(incremental ? 1 : 0); | |||
} | |||
@@ -58,9 +53,7 @@ it('renders status of short-living branch', () => { | |||
status: { bugs: 0, codeSmells: 2, vulnerabilities: 3 }, | |||
type: BranchType.SHORT | |||
}; | |||
expect( | |||
shallow(<ComponentNavMeta branch={branch} component={component} conf={{}} />) | |||
).toMatchSnapshot(); | |||
expect(shallow(<ComponentNavMeta branch={branch} component={component} />)).toMatchSnapshot(); | |||
}); | |||
it('renders meta for long-living branch', () => { | |||
@@ -70,7 +63,5 @@ it('renders meta for long-living branch', () => { | |||
status: { qualityGateStatus: 'OK' }, | |||
type: BranchType.LONG | |||
}; | |||
expect( | |||
shallow(<ComponentNavMeta branch={branch} component={component} conf={{}} />) | |||
).toMatchSnapshot(); | |||
expect(shallow(<ComponentNavMeta branch={branch} component={component} />)).toMatchSnapshot(); | |||
}); |
@@ -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(); |
@@ -1,26 +0,0 @@ | |||
/* | |||
* 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 NoBranchSupportPopup from '../NoBranchSupportPopup'; | |||
it('renders', () => { | |||
expect(shallow(<NoBranchSupportPopup />)).toMatchSnapshot(); | |||
}); |
@@ -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(); |
@@ -50,7 +50,6 @@ exports[`renders 1`] = ` | |||
"qualifier": "TRK", | |||
} | |||
} | |||
conf={Object {}} | |||
incremental={true} | |||
isFailed={true} | |||
isInProgress={true} | |||
@@ -72,7 +71,6 @@ exports[`renders 1`] = ` | |||
"qualifier": "TRK", | |||
} | |||
} | |||
conf={Object {}} | |||
location={Object {}} | |||
/> | |||
</ContextNavBar> |
@@ -26,47 +26,6 @@ exports[`renders main branch 1`] = ` | |||
</div> | |||
`; | |||
exports[`renders no branch support popup 1`] = ` | |||
<div | |||
className="navbar-context-branches" | |||
> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
"isMain": true, | |||
"name": "master", | |||
} | |||
} | |||
className="little-spacer-right" | |||
color="#cdcdcd" | |||
/> | |||
<span | |||
className="note" | |||
> | |||
master | |||
</span> | |||
<div | |||
className="display-inline-block spacer-left" | |||
> | |||
<a | |||
className="link-no-underline" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<HelpIcon | |||
fill="#cdcdcd" | |||
/> | |||
</a> | |||
<BubblePopupHelper | |||
isOpen={false} | |||
popup={<NoBranchSupportPopup />} | |||
position="bottomleft" | |||
togglePopup={[Function]} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`renders short-living branch 1`] = ` | |||
<div | |||
className="navbar-context-branches dropdown" |
@@ -39,6 +39,7 @@ exports[`renders list 1`] = ` | |||
"key": "component", | |||
} | |||
} | |||
onBranchesChange={[Function]} | |||
onSelect={[Function]} | |||
selected={true} | |||
/> | |||
@@ -78,6 +79,7 @@ exports[`renders list 1`] = ` | |||
"key": "component", | |||
} | |||
} | |||
onBranchesChange={[Function]} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
@@ -101,6 +103,7 @@ exports[`renders list 1`] = ` | |||
"key": "component", | |||
} | |||
} | |||
onBranchesChange={[Function]} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
@@ -120,6 +123,7 @@ exports[`renders list 1`] = ` | |||
"key": "component", | |||
} | |||
} | |||
onBranchesChange={[Function]} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
@@ -159,6 +163,7 @@ exports[`renders list 1`] = ` | |||
"key": "component", | |||
} | |||
} | |||
onBranchesChange={[Function]} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> | |||
@@ -213,6 +218,7 @@ exports[`searches 1`] = ` | |||
"key": "component", | |||
} | |||
} | |||
onBranchesChange={[Function]} | |||
onSelect={[Function]} | |||
selected={true} | |||
/> | |||
@@ -232,6 +238,7 @@ exports[`searches 1`] = ` | |||
"key": "component", | |||
} | |||
} | |||
onBranchesChange={[Function]} | |||
onSelect={[Function]} | |||
selected={false} | |||
/> |
@@ -17,7 +17,9 @@ exports[`renders main branch 1`] = ` | |||
} | |||
} | |||
> | |||
<div> | |||
<div | |||
className="navbar-context-meta-branch-menu-item-name" | |||
> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
@@ -70,7 +72,9 @@ exports[`renders short-living branch 1`] = ` | |||
} | |||
} | |||
> | |||
<div> | |||
<div | |||
className="navbar-context-meta-branch-menu-item-name" | |||
> | |||
<BranchIcon | |||
branch={ | |||
Object { | |||
@@ -132,7 +136,9 @@ exports[`renders short-living orhpan branch 1`] = ` | |||
} | |||
} | |||
> | |||
<div> | |||
<div | |||
className="navbar-context-meta-branch-menu-item-name" | |||
> | |||
<BranchIcon | |||
branch={ | |||
Object { |
@@ -129,23 +129,6 @@ 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" | |||
@@ -755,8 +738,7 @@ exports[`should work for long-living branches 1`] = ` | |||
} | |||
} | |||
> | |||
layout.settings | |||
branches.branch_settings | |||
</Link> | |||
</li> | |||
</NavBarTabs> | |||
@@ -1029,23 +1011,6 @@ 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" | |||
@@ -1251,23 +1216,6 @@ 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" |
@@ -1,37 +0,0 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<BubblePopup | |||
customClass="bubble-popup-bottom" | |||
> | |||
<div | |||
className="abs-width-400" | |||
> | |||
<h6 | |||
className="spacer-bottom" | |||
> | |||
branches.no_support.header | |||
</h6> | |||
<p | |||
className="big-spacer-bottom markdown" | |||
> | |||
branches.no_support.header.text | |||
</p> | |||
<p> | |||
<a | |||
href="https://redirect.sonarsource.com/doc/branches.html" | |||
target="_blank" | |||
> | |||
learn_more | |||
</a> | |||
<a | |||
className="button spacer-left" | |||
href="https://www.sonarsource.com/company/contact/" | |||
target="_blank" | |||
> | |||
buy_developer_pack | |||
</a> | |||
</p> | |||
</div> | |||
</BubblePopup> | |||
`; |
@@ -79,7 +79,7 @@ export interface Component { | |||
version?: string; | |||
} | |||
export interface ComponentConfiguration { | |||
interface ComponentConfiguration { | |||
extensions?: ComponentExtension[]; | |||
showBackgroundTasks?: boolean; | |||
showLinks?: boolean; |
@@ -55,7 +55,6 @@ 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'; | |||
@@ -207,7 +206,6 @@ 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> |
@@ -56,19 +56,23 @@ export default class App extends React.PureComponent { | |||
query: { id: this.props.component.key } | |||
}); | |||
} | |||
if (isShortLivingBranch(this.props.branch)) { | |||
if (isShortLivingBranch(this.props.branch) && !this.isFile()) { | |||
this.context.router.replace(getProjectBranchUrl(this.props.component.key, this.props.branch)); | |||
} | |||
} | |||
isPortfolio() { | |||
return this.props.component.qualifier === 'VW' || this.props.component.qualifier === 'SVW'; | |||
return ['VW', 'SVW'].includes(this.props.component.qualifier); | |||
} | |||
isFile() { | |||
return ['FIL', 'UTS'].includes(this.props.component.qualifier); | |||
} | |||
render() { | |||
const { branch, component } = this.props; | |||
if (this.isPortfolio() || isShortLivingBranch(branch)) { | |||
if (this.isPortfolio() || (isShortLivingBranch(branch) && !this.isFile())) { | |||
return null; | |||
} | |||
@@ -1,60 +0,0 @@ | |||
/* | |||
* 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> | |||
); | |||
} |
@@ -1,139 +0,0 @@ | |||
/* | |||
* 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> | |||
); | |||
} | |||
} |
@@ -1,34 +0,0 @@ | |||
/* | |||
* 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(); | |||
}); |
@@ -1,65 +0,0 @@ | |||
/* | |||
* 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; | |||
} |
@@ -1,73 +0,0 @@ | |||
// 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> | |||
`; |
@@ -1,100 +0,0 @@ | |||
// 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> | |||
`; |
@@ -1,30 +0,0 @@ | |||
/* | |||
* 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; |
@@ -34,6 +34,7 @@ interface Props { | |||
} | |||
interface State { | |||
advanced: boolean; | |||
branch: string; | |||
createdProject?: { key: string; name: string }; | |||
key: string; | |||
@@ -50,6 +51,7 @@ export default class CreateProjectForm extends React.PureComponent<Props, State> | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
advanced: false, | |||
branch: '', | |||
key: '', | |||
loading: false, | |||
@@ -71,6 +73,12 @@ export default class CreateProjectForm extends React.PureComponent<Props, State> | |||
this.props.onClose(); | |||
}; | |||
handleAdvancedClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.setState(state => ({ advanced: !state.advanced })); | |||
}; | |||
handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => { | |||
const { name, value } = event.currentTarget; | |||
this.setState({ [name]: value }); | |||
@@ -162,17 +170,6 @@ export default class CreateProjectForm extends React.PureComponent<Props, State> | |||
value={this.state.name} | |||
/> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="create-project-branch">{translate('branch')}</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={this.handleInputChange} | |||
type="text" | |||
value={this.state.branch} | |||
/> | |||
</div> | |||
<div className="modal-field"> | |||
<label htmlFor="create-project-key"> | |||
{translate('key')} | |||
@@ -202,6 +199,29 @@ export default class CreateProjectForm extends React.PureComponent<Props, State> | |||
</div> | |||
)} | |||
</div> | |||
{this.state.advanced ? ( | |||
<div className="modal-field big-spacer-top"> | |||
<label htmlFor="create-project-branch">{translate('branch')}</label> | |||
<input | |||
autoFocus={true} | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={this.handleInputChange} | |||
type="text" | |||
value={this.state.branch} | |||
/> | |||
</div> | |||
) : ( | |||
<div className="modal-field big-spacer-top"> | |||
<a | |||
className="js-more text-muted note" | |||
href="#" | |||
onClick={this.handleAdvancedClick}> | |||
{translate('more')} | |||
</a> | |||
</div> | |||
)} | |||
</div> | |||
<footer className="modal-foot"> |
@@ -26,7 +26,7 @@ jest.mock('../../../api/components', () => ({ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import CreateProjectForm from '../CreateProjectForm'; | |||
import { change, submit } from '../../../helpers/testUtils'; | |||
import { change, submit, click } from '../../../helpers/testUtils'; | |||
const createProject = require('../../../api/components').createProject as jest.Mock<any>; | |||
@@ -46,9 +46,6 @@ it('creates project', async () => { | |||
change(wrapper.find('input[name="name"]'), 'name', { | |||
currentTarget: { name: 'name', value: 'name' } | |||
}); | |||
change(wrapper.find('input[name="branch"]'), 'branch', { | |||
currentTarget: { name: 'branch', value: 'branch' } | |||
}); | |||
change(wrapper.find('input[name="key"]'), 'key', { | |||
currentTarget: { name: 'key', value: 'key' } | |||
}); | |||
@@ -58,7 +55,7 @@ it('creates project', async () => { | |||
submit(wrapper.find('form')); | |||
expect(createProject).toBeCalledWith({ | |||
branch: 'branch', | |||
branch: '', | |||
name: 'name', | |||
organization: 'org', | |||
project: 'key', | |||
@@ -66,7 +63,21 @@ it('creates project', async () => { | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
await new Promise(resolve => setImmediate(resolve)); | |||
await new Promise(setImmediate); | |||
wrapper.update(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('shows more', () => { | |||
const wrapper = shallow( | |||
<CreateProjectForm | |||
onClose={jest.fn()} | |||
onProjectCreated={jest.fn()} | |||
organization={organization} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('.js-more')); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -52,23 +52,6 @@ exports[`creates project 1`] = ` | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-branch" | |||
> | |||
branch | |||
</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={[Function]} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
@@ -111,6 +94,17 @@ exports[`creates project 1`] = ` | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field big-spacer-top" | |||
> | |||
<a | |||
className="js-more text-muted note" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
more | |||
</a> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
@@ -186,23 +180,6 @@ exports[`creates project 2`] = ` | |||
value="name" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-branch" | |||
> | |||
branch | |||
</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={[Function]} | |||
type="text" | |||
value="branch" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
@@ -245,6 +222,17 @@ exports[`creates project 2`] = ` | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field big-spacer-top" | |||
> | |||
<a | |||
className="js-more text-muted note" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
more | |||
</a> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
@@ -320,23 +308,6 @@ exports[`creates project 3`] = ` | |||
value="name" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-branch" | |||
> | |||
branch | |||
</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={[Function]} | |||
type="text" | |||
value="branch" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
@@ -379,6 +350,17 @@ exports[`creates project 3`] = ` | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field big-spacer-top" | |||
> | |||
<a | |||
className="js-more text-muted note" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
more | |||
</a> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
@@ -467,3 +449,266 @@ exports[`creates project 4`] = ` | |||
</div> | |||
</Modal> | |||
`; | |||
exports[`shows more 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<form | |||
id="create-project-form" | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
qualifiers.create.TRK | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-name" | |||
> | |||
name | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-project-name" | |||
maxLength={2000} | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-key" | |||
> | |||
key | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
id="create-project-key" | |||
maxLength={400} | |||
name="key" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
visibility | |||
</label> | |||
<VisibilitySelector | |||
className="little-spacer-top" | |||
onChange={[Function]} | |||
visibility="public" | |||
/> | |||
<div | |||
className="spacer-top" | |||
> | |||
<UpgradeOrganizationBox | |||
organization="org" | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field big-spacer-top" | |||
> | |||
<a | |||
className="js-more text-muted note" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
more | |||
</a> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={false} | |||
id="create-project-submit" | |||
type="submit" | |||
> | |||
create | |||
</button> | |||
<a | |||
href="#" | |||
id="create-project-cancel" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; | |||
exports[`shows more 2`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<form | |||
id="create-project-form" | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
qualifiers.create.TRK | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-name" | |||
> | |||
name | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-project-name" | |||
maxLength={2000} | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-key" | |||
> | |||
key | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
id="create-project-key" | |||
maxLength={400} | |||
name="key" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
visibility | |||
</label> | |||
<VisibilitySelector | |||
className="little-spacer-top" | |||
onChange={[Function]} | |||
visibility="public" | |||
/> | |||
<div | |||
className="spacer-top" | |||
> | |||
<UpgradeOrganizationBox | |||
organization="org" | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="modal-field big-spacer-top" | |||
> | |||
<label | |||
htmlFor="create-project-branch" | |||
> | |||
branch | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={[Function]} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={false} | |||
id="create-project-submit" | |||
type="submit" | |||
> | |||
create | |||
</button> | |||
<a | |||
href="#" | |||
id="create-project-cancel" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; |
@@ -98,7 +98,7 @@ export default class App extends React.PureComponent { | |||
link: ( | |||
<Link | |||
to={{ | |||
pathname: '/project/branches', | |||
pathname: '/project/settings', | |||
query: { id: this.props.component && this.props.component.key } | |||
}}> | |||
{translate('branches.settings_hint_tab')} |
@@ -41,35 +41,35 @@ export default function BranchStatus({ branch, concise = false }: Props) { | |||
const totalIssues = | |||
branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells; | |||
return ( | |||
const indicatorClassName = classNames('branch-status-indicator', { | |||
'is-failed': totalIssues > 0, | |||
'is-passed': totalIssues === 0 | |||
}); | |||
return concise ? ( | |||
<ul className="list-inline branch-status"> | |||
<li>{totalIssues}</li> | |||
<li className="spacer-left"> | |||
<i className={indicatorClassName} /> | |||
</li> | |||
</ul> | |||
) : ( | |||
<ul className="list-inline branch-status"> | |||
<li className="spacer-right"> | |||
<i className={indicatorClassName} /> | |||
</li> | |||
<li> | |||
{branch.status.bugs} | |||
<BugIcon /> | |||
</li> | |||
<li> | |||
{branch.status.vulnerabilities} | |||
<VulnerabilityIcon /> | |||
</li> | |||
<li> | |||
<i | |||
className={classNames('branch-status-indicator', { | |||
'is-failed': totalIssues > 0, | |||
'is-passed': totalIssues === 0 | |||
})} | |||
/> | |||
{branch.status.codeSmells} | |||
<CodeSmellIcon /> | |||
</li> | |||
{concise && <li>{totalIssues}</li>} | |||
{!concise && ( | |||
<li> | |||
{branch.status.bugs} | |||
<BugIcon className="little-spacer-left" /> | |||
</li> | |||
)} | |||
{!concise && ( | |||
<li> | |||
{branch.status.vulnerabilities} | |||
<VulnerabilityIcon className="little-spacer-left" /> | |||
</li> | |||
)} | |||
{!concise && ( | |||
<li> | |||
{branch.status.codeSmells} | |||
<CodeSmellIcon className="little-spacer-left" /> | |||
</li> | |||
)} | |||
</ul> | |||
); | |||
} else { |
@@ -20,28 +20,24 @@ exports[`renders status of short-living branches 1`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> | |||
<li> | |||
<li | |||
className="spacer-right" | |||
> | |||
<i | |||
className="branch-status-indicator is-passed" | |||
/> | |||
</li> | |||
<li> | |||
0 | |||
<BugIcon | |||
className="little-spacer-left" | |||
/> | |||
<BugIcon /> | |||
</li> | |||
<li> | |||
0 | |||
<VulnerabilityIcon | |||
className="little-spacer-left" | |||
/> | |||
<VulnerabilityIcon /> | |||
</li> | |||
<li> | |||
0 | |||
<CodeSmellIcon | |||
className="little-spacer-left" | |||
/> | |||
<CodeSmellIcon /> | |||
</li> | |||
</ul> | |||
`; | |||
@@ -50,28 +46,24 @@ exports[`renders status of short-living branches 2`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> | |||
<li> | |||
<li | |||
className="spacer-right" | |||
> | |||
<i | |||
className="branch-status-indicator is-failed" | |||
/> | |||
</li> | |||
<li> | |||
0 | |||
<BugIcon | |||
className="little-spacer-left" | |||
/> | |||
<BugIcon /> | |||
</li> | |||
<li> | |||
0 | |||
<VulnerabilityIcon | |||
className="little-spacer-left" | |||
/> | |||
<VulnerabilityIcon /> | |||
</li> | |||
<li> | |||
1 | |||
<CodeSmellIcon | |||
className="little-spacer-left" | |||
/> | |||
<CodeSmellIcon /> | |||
</li> | |||
</ul> | |||
`; | |||
@@ -80,28 +72,24 @@ exports[`renders status of short-living branches 3`] = ` | |||
<ul | |||
className="list-inline branch-status" | |||
> | |||
<li> | |||
<li | |||
className="spacer-right" | |||
> | |||
<i | |||
className="branch-status-indicator is-failed" | |||
/> | |||
</li> | |||
<li> | |||
7 | |||
<BugIcon | |||
className="little-spacer-left" | |||
/> | |||
<BugIcon /> | |||
</li> | |||
<li> | |||
6 | |||
<VulnerabilityIcon | |||
className="little-spacer-left" | |||
/> | |||
<VulnerabilityIcon /> | |||
</li> | |||
<li> | |||
3 | |||
<CodeSmellIcon | |||
className="little-spacer-left" | |||
/> | |||
<CodeSmellIcon /> | |||
</li> | |||
</ul> | |||
`; |
@@ -3178,13 +3178,13 @@ 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.settings_hint=To administrate your branches, you have to go to your main branch's {link} tab. | |||
branches.settings_hint_tab=Administration > Branches | |||
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 | |||
#------------------------------------------------------------------------------ |