@@ -157,10 +157,7 @@ export default class ProjectActivityAnalysesList extends React.PureComponent<Pro | |||
}; | |||
shouldRenderBaselineMarker(analysis: T.ParsedAnalysis): boolean { | |||
return Boolean( | |||
analysis.manualNewCodePeriodBaseline || | |||
(this.props.leakPeriodDate && isEqual(this.props.leakPeriodDate, analysis.date)) | |||
); | |||
return Boolean(this.props.leakPeriodDate && isEqual(this.props.leakPeriodDate, analysis.date)); | |||
} | |||
renderAnalysis(analysis: T.ParsedAnalysis) { |
@@ -40,15 +40,6 @@ it('should not display reset button if project setting is not set', () => { | |||
expect(wrapper.find('Button')).toHaveLength(0); | |||
}); | |||
it('should display reset button if project setting is set', async () => { | |||
(getNewCodePeriod as jest.Mock).mockResolvedValue({ type: 'NUMBER_OF_DAYS', value: '27' }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.find('Button')).toHaveLength(1); | |||
}); | |||
it('should reset the setting correctly', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); |
@@ -25,13 +25,22 @@ import ProjectBaselineSelector, { | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ branchesEnabled: false })).toMatchSnapshot(); | |||
expect( | |||
shallowRender({ | |||
branchesEnabled: false, | |||
generalSetting: { type: 'NUMBER_OF_DAYS', value: '23' } | |||
}) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallowRender({ branchesEnabled: false, generalSetting: { type: 'NUMBER_OF_DAYS', value: '' } }) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should not show save button when unchanged', () => { | |||
const wrapper = shallowRender({ | |||
currentSetting: 'PREVIOUS_VERSION', | |||
selected: 'PREVIOUS_VERSION' | |||
selected: 'PREVIOUS_VERSION', | |||
overrideGeneralSetting: true | |||
}); | |||
expect( | |||
wrapper | |||
@@ -42,7 +51,11 @@ it('should not show save button when unchanged', () => { | |||
}); | |||
it('should show save button when changed', () => { | |||
const wrapper = shallowRender({ currentSetting: 'PREVIOUS_VERSION', selected: 'NUMBER_OF_DAYS' }); | |||
const wrapper = shallowRender({ | |||
currentSetting: 'PREVIOUS_VERSION', | |||
selected: 'NUMBER_OF_DAYS', | |||
overrideGeneralSetting: true | |||
}); | |||
expect(wrapper.find('SubmitButton')).toHaveLength(1); | |||
}); | |||
@@ -51,7 +64,8 @@ it('should show save button when value changed', () => { | |||
currentSetting: 'NUMBER_OF_DAYS', | |||
currentSettingValue: '23', | |||
days: '25', | |||
selected: 'NUMBER_OF_DAYS' | |||
selected: 'NUMBER_OF_DAYS', | |||
overrideGeneralSetting: true | |||
}); | |||
expect(wrapper.find('SubmitButton')).toHaveLength(1); | |||
}); | |||
@@ -61,7 +75,8 @@ it('should disable the save button when saving', () => { | |||
currentSetting: 'NUMBER_OF_DAYS', | |||
currentSettingValue: '25', | |||
saving: true, | |||
selected: 'PREVIOUS_VERSION' | |||
selected: 'PREVIOUS_VERSION', | |||
overrideGeneralSetting: true | |||
}); | |||
expect( | |||
@@ -76,7 +91,8 @@ it('should disable the save button when date is invalid', () => { | |||
const wrapper = shallowRender({ | |||
currentSetting: 'PREVIOUS_VERSION', | |||
days: 'hello', | |||
selected: 'NUMBER_OF_DAYS' | |||
selected: 'NUMBER_OF_DAYS', | |||
overrideGeneralSetting: true | |||
}); | |||
expect( | |||
@@ -93,10 +109,13 @@ function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) { | |||
branchesEnabled={true} | |||
component="" | |||
days="12" | |||
generalSetting={{}} | |||
onSelectAnalysis={jest.fn()} | |||
onSelectDays={jest.fn()} | |||
onSelectSetting={jest.fn()} | |||
onSubmit={jest.fn()} | |||
onToggleSpecificSetting={jest.fn()} | |||
overrideGeneralSetting={false} | |||
saving={false} | |||
{...props} | |||
/> |
@@ -9,11 +9,6 @@ exports[`should render correctly 1`] = ` | |||
<tr> | |||
<th> | |||
branch_list.branch | |||
</th> | |||
<th | |||
className="thin" | |||
> | |||
</th> | |||
<th | |||
className="thin nowrap huge-spacer-right" | |||
@@ -56,7 +51,6 @@ exports[`should render correctly 1`] = ` | |||
branches.main_branch | |||
</div> | |||
</td> | |||
<td /> | |||
<td | |||
className="huge-spacer-right nowrap" | |||
> | |||
@@ -98,17 +92,10 @@ exports[`should render correctly 1`] = ` | |||
/> | |||
branch-6.7 | |||
</td> | |||
<td> | |||
<span | |||
className="badge badge-info" | |||
> | |||
default | |||
</span> | |||
</td> | |||
<td | |||
className="huge-spacer-right nowrap" | |||
> | |||
baseline.previous_version | |||
branch_list.default_setting | |||
</td> | |||
<td | |||
className="text-right" |
@@ -6,20 +6,56 @@ exports[`should render correctly 1`] = ` | |||
onSubmit={[MockFunction]} | |||
> | |||
<div | |||
className="branch-baseline-setting-modal" | |||
className="big-spacer-top spacer-bottom" | |||
role="radiogroup" | |||
> | |||
<Radio | |||
checked={true} | |||
className="big-spacer-bottom" | |||
onCheck={[Function]} | |||
value="general" | |||
> | |||
project_baseline.general_setting | |||
</Radio> | |||
<div | |||
className="big-spacer-left" | |||
> | |||
<div | |||
className="general-setting" | |||
> | |||
<strong> | |||
baseline.previous_version | |||
</strong> | |||
: | |||
baseline.previous_version.description | |||
</div> | |||
</div> | |||
<Radio | |||
checked={false} | |||
className="huge-spacer-top" | |||
onCheck={[Function]} | |||
value="specific" | |||
> | |||
project_baseline.specific_setting | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-left big-spacer-right branch-baseline-setting-modal" | |||
> | |||
<div | |||
className="display-flex-row big-spacer-bottom" | |||
role="radiogroup" | |||
> | |||
<BaselineSettingPreviousVersion | |||
disabled={true} | |||
onSelect={[MockFunction]} | |||
selected={false} | |||
/> | |||
<BaselineSettingDays | |||
days="12" | |||
disabled={true} | |||
isChanged={false} | |||
isValid={false} | |||
isValid={true} | |||
onChangeDays={[MockFunction]} | |||
onSelect={[MockFunction]} | |||
selected={false} | |||
@@ -54,25 +90,151 @@ exports[`should render correctly 2`] = ` | |||
onSubmit={[MockFunction]} | |||
> | |||
<div | |||
className="branch-baseline-setting-modal" | |||
className="big-spacer-top spacer-bottom" | |||
role="radiogroup" | |||
> | |||
<Radio | |||
checked={true} | |||
className="big-spacer-bottom" | |||
onCheck={[Function]} | |||
value="general" | |||
> | |||
project_baseline.general_setting | |||
</Radio> | |||
<div | |||
className="big-spacer-left" | |||
> | |||
<div | |||
className="general-setting" | |||
> | |||
<strong> | |||
baseline.number_days (duration.days.23) | |||
</strong> | |||
: | |||
baseline.number_days.description | |||
</div> | |||
</div> | |||
<Radio | |||
checked={false} | |||
className="huge-spacer-top" | |||
onCheck={[Function]} | |||
value="specific" | |||
> | |||
project_baseline.specific_setting | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-left big-spacer-right branch-baseline-setting-modal" | |||
> | |||
<div | |||
className="display-flex-row big-spacer-bottom" | |||
role="radiogroup" | |||
> | |||
<BaselineSettingPreviousVersion | |||
disabled={true} | |||
onSelect={[MockFunction]} | |||
selected={false} | |||
/> | |||
<BaselineSettingDays | |||
days="12" | |||
disabled={true} | |||
isChanged={false} | |||
isValid={true} | |||
onChangeDays={[MockFunction]} | |||
onSelect={[MockFunction]} | |||
selected={false} | |||
/> | |||
<BaselineSettingAnalysis | |||
disabled={true} | |||
onSelect={[MockFunction]} | |||
selected={false} | |||
/> | |||
</div> | |||
</div> | |||
<div | |||
className="big-spacer-top invisible" | |||
> | |||
<p | |||
className="spacer-bottom" | |||
> | |||
baseline.next_analysis_notice | |||
</p> | |||
<DeferredSpinner | |||
className="spacer-right" | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<SubmitButton | |||
disabled={true} | |||
> | |||
save | |||
</SubmitButton> | |||
</div> | |||
</form> | |||
`; | |||
exports[`should render correctly 3`] = ` | |||
<form | |||
className="project-baseline-selector" | |||
onSubmit={[MockFunction]} | |||
> | |||
<div | |||
className="big-spacer-top spacer-bottom" | |||
role="radiogroup" | |||
> | |||
<Radio | |||
checked={true} | |||
className="big-spacer-bottom" | |||
onCheck={[Function]} | |||
value="general" | |||
> | |||
project_baseline.general_setting | |||
</Radio> | |||
<div | |||
className="big-spacer-left" | |||
> | |||
<div | |||
className="general-setting" | |||
> | |||
<strong> | |||
baseline.number_days (duration.days.?) | |||
</strong> | |||
: | |||
baseline.number_days.description | |||
</div> | |||
</div> | |||
<Radio | |||
checked={false} | |||
className="huge-spacer-top" | |||
onCheck={[Function]} | |||
value="specific" | |||
> | |||
project_baseline.specific_setting | |||
</Radio> | |||
</div> | |||
<div | |||
className="big-spacer-left big-spacer-right branch-baseline-setting-modal" | |||
> | |||
<div | |||
className="display-flex-row big-spacer-bottom" | |||
role="radiogroup" | |||
> | |||
<BaselineSettingPreviousVersion | |||
disabled={true} | |||
onSelect={[MockFunction]} | |||
selected={false} | |||
/> | |||
<BaselineSettingDays | |||
days="12" | |||
disabled={true} | |||
isChanged={false} | |||
isValid={false} | |||
isValid={true} | |||
onChangeDays={[MockFunction]} | |||
onSelect={[MockFunction]} | |||
selected={false} | |||
/> | |||
<BaselineSettingAnalysis | |||
disabled={true} | |||
onSelect={[MockFunction]} | |||
selected={false} | |||
/> |
@@ -17,7 +17,7 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { getSettingValue } from '../utils'; | |||
import { getSettingValue, validateSetting } from '../utils'; | |||
describe('getSettingValue', () => { | |||
it('should work for Days', () => { | |||
@@ -38,3 +38,78 @@ describe('getSettingValue', () => { | |||
).toBeUndefined(); | |||
}); | |||
}); | |||
describe('validateSettings', () => { | |||
it('should validate at branch level', () => { | |||
expect(validateSetting({ days: '' })).toEqual({ isChanged: false, isValid: false }); | |||
expect( | |||
validateSetting({ | |||
currentSetting: 'PREVIOUS_VERSION', | |||
days: '12', | |||
selected: 'NUMBER_OF_DAYS' | |||
}) | |||
).toEqual({ isChanged: true, isValid: true }); | |||
expect( | |||
validateSetting({ | |||
currentSetting: 'PREVIOUS_VERSION', | |||
days: 'nope', | |||
selected: 'NUMBER_OF_DAYS' | |||
}) | |||
).toEqual({ isChanged: true, isValid: false }); | |||
expect( | |||
validateSetting({ | |||
currentSetting: 'NUMBER_OF_DAYS', | |||
currentSettingValue: '15', | |||
days: '15', | |||
selected: 'NUMBER_OF_DAYS' | |||
}) | |||
).toEqual({ isChanged: false, isValid: true }); | |||
expect( | |||
validateSetting({ | |||
currentSetting: 'NUMBER_OF_DAYS', | |||
currentSettingValue: '15', | |||
days: '13', | |||
selected: 'NUMBER_OF_DAYS' | |||
}) | |||
).toEqual({ isChanged: true, isValid: true }); | |||
expect( | |||
validateSetting({ | |||
analysis: 'analysis1', | |||
currentSetting: 'SPECIFIC_ANALYSIS', | |||
currentSettingValue: 'analysis1', | |||
days: '', | |||
selected: 'SPECIFIC_ANALYSIS' | |||
}) | |||
).toEqual({ isChanged: false, isValid: true }); | |||
expect( | |||
validateSetting({ | |||
analysis: 'analysis2', | |||
currentSetting: 'SPECIFIC_ANALYSIS', | |||
currentSettingValue: 'analysis1', | |||
days: '', | |||
selected: 'SPECIFIC_ANALYSIS' | |||
}) | |||
).toEqual({ isChanged: true, isValid: true }); | |||
}); | |||
it('should validate at project level', () => { | |||
expect(validateSetting({ days: '', overrideGeneralSetting: false })).toEqual({ | |||
isChanged: false, | |||
isValid: true | |||
}); | |||
expect(validateSetting({ days: '', overrideGeneralSetting: true })).toEqual({ | |||
isChanged: true, | |||
isValid: false | |||
}); | |||
expect( | |||
validateSetting({ | |||
currentSetting: 'PREVIOUS_VERSION', | |||
days: '', | |||
overrideGeneralSetting: false | |||
}) | |||
).toEqual({ | |||
isChanged: true, | |||
isValid: true | |||
}); | |||
}); | |||
}); |
@@ -20,9 +20,8 @@ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod'; | |||
import '../styles.css'; | |||
import { getSettingValue } from '../utils'; | |||
@@ -43,6 +42,7 @@ interface State { | |||
days: string; | |||
generalSetting?: T.NewCodePeriod; | |||
loading: boolean; | |||
overrideGeneralSetting?: boolean; | |||
saving: boolean; | |||
selected?: T.NewCodePeriodSettingType; | |||
} | |||
@@ -75,13 +75,17 @@ export default class App extends React.PureComponent<Props, State> { | |||
}) { | |||
const { currentSetting, currentSettingValue, generalSetting } = params; | |||
const defaultDays = | |||
(!currentSetting && generalSetting.type === 'NUMBER_OF_DAYS' && generalSetting.value) || '30'; | |||
return { | |||
loading: false, | |||
currentSetting, | |||
currentSettingValue, | |||
generalSetting, | |||
selected: currentSetting, | |||
days: currentSetting === 'NUMBER_OF_DAYS' ? currentSettingValue || '30' : '', | |||
selected: currentSetting || generalSetting.type, | |||
overrideGeneralSetting: Boolean(currentSetting), | |||
days: (currentSetting === 'NUMBER_OF_DAYS' && currentSettingValue) || defaultDays, | |||
analysis: (currentSetting === 'SPECIFIC_ANALYSIS' && currentSettingValue) || '' | |||
}; | |||
} | |||
@@ -92,7 +96,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
Promise.all([ | |||
getNewCodePeriod(), | |||
getNewCodePeriod({ | |||
branch: this.props.branchesEnabled ? 'master' : undefined, | |||
branch: !this.props.branchesEnabled ? 'master' : undefined, | |||
project: this.props.component.key | |||
}) | |||
]).then( | |||
@@ -137,11 +141,19 @@ export default class App extends React.PureComponent<Props, State> { | |||
handleSelectSetting = (selected?: T.NewCodePeriodSettingType) => this.setState({ selected }); | |||
handleToggleSpecificSetting = (overrideGeneralSetting: boolean) => | |||
this.setState({ overrideGeneralSetting }); | |||
handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => { | |||
e.preventDefault(); | |||
const { component } = this.props; | |||
const { analysis, days, selected: type } = this.state; | |||
const { analysis, days, selected: type, overrideGeneralSetting } = this.state; | |||
if (!overrideGeneralSetting) { | |||
this.resetSetting(); | |||
return; | |||
} | |||
const value = getSettingValue({ type, analysis, days }); | |||
@@ -201,17 +213,6 @@ export default class App extends React.PureComponent<Props, State> { | |||
); | |||
} | |||
renderGeneralSetting(generalSetting: T.NewCodePeriod) { | |||
if (generalSetting.type === 'NUMBER_OF_DAYS') { | |||
return `${translate('baseline.number_days')} (${translateWithParameters( | |||
'duration.days', | |||
generalSetting.value || '?' | |||
)})`; | |||
} else { | |||
return translate('baseline.previous_version'); | |||
} | |||
} | |||
render() { | |||
const { branchLikes, branchesEnabled, component } = this.props; | |||
const { | |||
@@ -221,6 +222,7 @@ export default class App extends React.PureComponent<Props, State> { | |||
generalSetting, | |||
loading, | |||
currentSettingValue, | |||
overrideGeneralSetting, | |||
saving, | |||
selected | |||
} = this.state; | |||
@@ -231,57 +233,45 @@ export default class App extends React.PureComponent<Props, State> { | |||
{loading ? ( | |||
<DeferredSpinner /> | |||
) : ( | |||
<div className="panel panel-white"> | |||
{branchesEnabled && ( | |||
<> | |||
<h2>{translate('project_baseline.default_setting')}</h2> | |||
<p>{translate('project_baseline.default_setting.description')}</p> | |||
</> | |||
)} | |||
<div className="panel-white project-baseline"> | |||
{branchesEnabled && <h2>{translate('project_baseline.default_setting')}</h2>} | |||
{generalSetting && ( | |||
<div className="text-right spacer-bottom"> | |||
{currentSetting && ( | |||
<> | |||
<Button className="little-spacer-bottom" onClick={this.resetSetting}> | |||
{translate('project_baseline.reset_to_general')} | |||
</Button> | |||
</> | |||
)} | |||
<div className="spacer-top medium"> | |||
<strong>{translate('project_baseline.general_setting')}: </strong> | |||
{this.renderGeneralSetting(generalSetting)} | |||
</div> | |||
</div> | |||
{generalSetting && overrideGeneralSetting !== undefined && ( | |||
<ProjectBaselineSelector | |||
analysis={analysis} | |||
branchesEnabled={branchesEnabled} | |||
component={component.key} | |||
currentSetting={currentSetting} | |||
currentSettingValue={currentSettingValue} | |||
days={days} | |||
generalSetting={generalSetting} | |||
onSelectAnalysis={this.handleSelectAnalysis} | |||
onSelectDays={this.handleSelectDays} | |||
onSelectSetting={this.handleSelectSetting} | |||
onSubmit={this.handleSubmit} | |||
onToggleSpecificSetting={this.handleToggleSpecificSetting} | |||
overrideGeneralSetting={overrideGeneralSetting} | |||
saving={saving} | |||
selected={selected} | |||
/> | |||
)} | |||
<ProjectBaselineSelector | |||
analysis={analysis} | |||
branchesEnabled={branchesEnabled} | |||
component={component.key} | |||
currentSetting={currentSetting} | |||
currentSettingValue={currentSettingValue} | |||
days={days} | |||
onSelectAnalysis={this.handleSelectAnalysis} | |||
onSelectDays={this.handleSelectDays} | |||
onSelectSetting={this.handleSelectSetting} | |||
onSubmit={this.handleSubmit} | |||
saving={saving} | |||
selected={selected} | |||
/> | |||
{generalSetting && branchesEnabled && ( | |||
<BranchList | |||
branchLikes={branchLikes} | |||
component={component} | |||
inheritedSetting={ | |||
currentSetting | |||
? { | |||
type: currentSetting, | |||
value: currentSettingValue | |||
} | |||
: generalSetting | |||
} | |||
/> | |||
<div className="huge-spacer-top branch-baseline-selector"> | |||
<hr /> | |||
<h2>{translate('project_baseline.configure_branches')}</h2> | |||
<BranchList | |||
branchLikes={branchLikes} | |||
component={component} | |||
inheritedSetting={ | |||
currentSetting | |||
? { | |||
type: currentSetting, | |||
value: currentSettingValue | |||
} | |||
: generalSetting | |||
} | |||
/> | |||
</div> | |||
)} | |||
</div> | |||
)} |
@@ -22,13 +22,15 @@ import RadioCard from 'sonar-ui-common/components/controls/RadioCard'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
export interface Props { | |||
disabled?: boolean; | |||
onSelect: (selection: T.NewCodePeriodSettingType) => void; | |||
selected: boolean; | |||
} | |||
export default function BaselineSettingAnalysis({ onSelect, selected }: Props) { | |||
export default function BaselineSettingAnalysis({ disabled, onSelect, selected }: Props) { | |||
return ( | |||
<RadioCard | |||
disabled={disabled} | |||
onClick={() => onSelect('SPECIFIC_ANALYSIS')} | |||
selected={selected} | |||
title={translate('baseline.specific_analysis')}> |
@@ -25,6 +25,7 @@ import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
export interface Props { | |||
className?: string; | |||
days: string; | |||
disabled?: boolean; | |||
isChanged: boolean; | |||
isValid: boolean; | |||
onChangeDays: (value: string) => void; | |||
@@ -33,10 +34,11 @@ export interface Props { | |||
} | |||
export default function BaselineSettingDays(props: Props) { | |||
const { className, days, isChanged, isValid, onChangeDays, onSelect, selected } = props; | |||
const { className, days, disabled, isChanged, isValid, onChangeDays, onSelect, selected } = props; | |||
return ( | |||
<RadioCard | |||
className={className} | |||
disabled={disabled} | |||
onClick={() => onSelect('NUMBER_OF_DAYS')} | |||
selected={selected} | |||
title={translate('baseline.number_days')}> |
@@ -22,14 +22,17 @@ import RadioCard from 'sonar-ui-common/components/controls/RadioCard'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
export interface Props { | |||
disabled?: boolean; | |||
isDefault?: boolean; | |||
onSelect: (selection: T.NewCodePeriodSettingType) => void; | |||
selected: boolean; | |||
} | |||
export default function BaselineSettingPreviousVersion({ isDefault, onSelect, selected }: Props) { | |||
export default function BaselineSettingPreviousVersion(props: Props) { | |||
const { disabled, isDefault, onSelect, selected } = props; | |||
return ( | |||
<RadioCard | |||
disabled={disabled} | |||
onClick={() => onSelect('PREVIOUS_VERSION')} | |||
selected={selected} | |||
title={ |
@@ -26,6 +26,7 @@ import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { parseDate, toShortNotSoISOString } from 'sonar-ui-common/helpers/dates'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { scrollToElement } from 'sonar-ui-common/helpers/scrolling'; | |||
import { getProjectActivity } from '../../../api/projectActivity'; | |||
import DateFormatter from '../../../components/intl/DateFormatter'; | |||
import TimeFormatter from '../../../components/intl/TimeFormatter'; | |||
@@ -49,6 +50,7 @@ interface State { | |||
export default class BranchAnalysisList extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
badges: T.Dict<HTMLDivElement> = {}; | |||
rootNodeRef: React.RefObject<HTMLDivElement>; | |||
state: State = { | |||
analyses: [], | |||
loading: true, | |||
@@ -58,6 +60,7 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State | |||
constructor(props: Props) { | |||
super(props); | |||
this.rootNodeRef = React.createRef<HTMLDivElement>(); | |||
this.updateScroll = throttle(this.updateScroll, 20); | |||
} | |||
@@ -70,6 +73,13 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State | |||
this.mounted = false; | |||
} | |||
scrollToSelected() { | |||
const selectedNode = document.querySelector('.branch-analysis.selected'); | |||
if (this.rootNodeRef.current && selectedNode) { | |||
scrollToElement(selectedNode, { parent: this.rootNodeRef.current, bottomOffset: 40 }); | |||
} | |||
} | |||
fetchAnalyses(initial = false) { | |||
const { analysis, branch, component } = this.props; | |||
const { range } = this.state; | |||
@@ -86,13 +96,18 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State | |||
return; | |||
} | |||
this.setState({ | |||
analyses: result.analyses.map(analysis => ({ | |||
...analysis, | |||
date: parseDate(analysis.date) | |||
})) as T.ParsedAnalysis[], | |||
loading: false | |||
}); | |||
this.setState( | |||
{ | |||
analyses: result.analyses.map(analysis => ({ | |||
...analysis, | |||
date: parseDate(analysis.date) | |||
})) as T.ParsedAnalysis[], | |||
loading: false | |||
}, | |||
() => { | |||
this.scrollToSelected(); | |||
} | |||
); | |||
}); | |||
} | |||
@@ -115,11 +130,9 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State | |||
} | |||
}; | |||
shouldStick = (version: string, index: number) => { | |||
shouldStick = (version: string) => { | |||
const badge = this.badges[version]; | |||
return ( | |||
badge && Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + 18 + index * 2 | |||
); | |||
return badge && Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + 10; | |||
}; | |||
getRangeOptions() { | |||
@@ -168,7 +181,10 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State | |||
/> | |||
</div> | |||
<div className="branch-analysis-list-wrapper"> | |||
<div className="bordered branch-analysis-list" onScroll={this.handleScroll}> | |||
<div | |||
className="bordered branch-analysis-list" | |||
onScroll={this.handleScroll} | |||
ref={this.rootNodeRef}> | |||
{loading && <DeferredSpinner className="big-spacer-top" />} | |||
{!loading && !hasFilteredData ? ( | |||
@@ -188,7 +204,7 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State | |||
<div | |||
className={classNames('branch-analysis-version-badge', { | |||
first: idx === 0, | |||
sticky: this.shouldStick(version.version, idx) | |||
sticky: this.shouldStick(version.version) | |||
})} | |||
ref={this.registerBadgeNode(version.version)}> | |||
<Tooltip | |||
@@ -212,7 +228,7 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State | |||
version.byDay[day].map(analysis => ( | |||
<li | |||
className={classNames('branch-analysis', { | |||
selected: false | |||
selected: analysis.key === this.props.analysis | |||
})} | |||
data-date={parseDate(analysis.date).valueOf()} | |||
key={analysis.key} |
@@ -168,7 +168,6 @@ export default class BranchList extends React.PureComponent<Props, State> { | |||
<thead> | |||
<tr> | |||
<th>{translate('branch_list.branch')}</th> | |||
<th className="thin"> </th> | |||
<th className="thin nowrap huge-spacer-right"> | |||
{translate('branch_list.current_setting')} | |||
</th> | |||
@@ -185,15 +184,10 @@ export default class BranchList extends React.PureComponent<Props, State> { | |||
<div className="badge spacer-left">{translate('branches.main_branch')}</div> | |||
)} | |||
</td> | |||
<td> | |||
{!branch.newCodePeriod && ( | |||
<span className="badge badge-info">{translate('default')}</span> | |||
)} | |||
</td> | |||
<td className="huge-spacer-right nowrap"> | |||
{branch.newCodePeriod | |||
? this.renderNewCodePeriodSetting(branch.newCodePeriod) | |||
: this.renderNewCodePeriodSetting(this.props.inheritedSetting)} | |||
: translate('branch_list.default_setting')} | |||
</td> | |||
<td className="text-right"> | |||
<ActionsDropdown> |
@@ -20,8 +20,9 @@ | |||
import * as classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import Radio from 'sonar-ui-common/components/controls/Radio'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { validateSetting } from '../utils'; | |||
import BaselineSettingAnalysis from './BaselineSettingAnalysis'; | |||
import BaselineSettingDays from './BaselineSettingDays'; | |||
@@ -35,12 +36,36 @@ export interface ProjectBaselineSelectorProps { | |||
currentSetting?: T.NewCodePeriodSettingType; | |||
currentSettingValue?: string; | |||
days: string; | |||
generalSetting: T.NewCodePeriod; | |||
onSelectAnalysis: (analysis: T.ParsedAnalysis) => void; | |||
onSelectDays: (value: string) => void; | |||
onSelectSetting: (value: T.NewCodePeriodSettingType) => void; | |||
onSelectSetting: (value?: T.NewCodePeriodSettingType) => void; | |||
onSubmit: (e: React.SyntheticEvent<HTMLFormElement>) => void; | |||
onToggleSpecificSetting: (selection: boolean) => void; | |||
saving: boolean; | |||
selected?: T.NewCodePeriodSettingType; | |||
overrideGeneralSetting: boolean; | |||
} | |||
function renderGeneralSetting(generalSetting: T.NewCodePeriod) { | |||
let setting: string; | |||
let description: string; | |||
if (generalSetting.type === 'NUMBER_OF_DAYS') { | |||
setting = `${translate('baseline.number_days')} (${translateWithParameters( | |||
'duration.days', | |||
generalSetting.value || '?' | |||
)})`; | |||
description = translate('baseline.number_days.description'); | |||
} else { | |||
setting = translate('baseline.previous_version'); | |||
description = translate('baseline.previous_version.description'); | |||
} | |||
return ( | |||
<div className="general-setting"> | |||
<strong>{setting}</strong>: {description} | |||
</div> | |||
); | |||
} | |||
export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) { | |||
@@ -49,10 +74,12 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr | |||
branchesEnabled, | |||
component, | |||
currentSetting, | |||
days, | |||
currentSettingValue, | |||
days, | |||
generalSetting, | |||
saving, | |||
selected | |||
selected, | |||
overrideGeneralSetting | |||
} = props; | |||
const { isChanged, isValid } = validateSetting({ | |||
@@ -60,29 +87,52 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr | |||
currentSetting, | |||
currentSettingValue, | |||
days, | |||
selected | |||
selected, | |||
overrideGeneralSetting | |||
}); | |||
return ( | |||
<form className="project-baseline-selector" onSubmit={props.onSubmit}> | |||
<div className="branch-baseline-setting-modal"> | |||
<div className="big-spacer-top spacer-bottom" role="radiogroup"> | |||
<Radio | |||
checked={!overrideGeneralSetting} | |||
className="big-spacer-bottom" | |||
onCheck={() => props.onToggleSpecificSetting(false)} | |||
value="general"> | |||
{translate('project_baseline.general_setting')} | |||
</Radio> | |||
<div className="big-spacer-left">{renderGeneralSetting(generalSetting)}</div> | |||
<Radio | |||
checked={overrideGeneralSetting} | |||
className="huge-spacer-top" | |||
onCheck={() => props.onToggleSpecificSetting(true)} | |||
value="specific"> | |||
{translate('project_baseline.specific_setting')} | |||
</Radio> | |||
</div> | |||
<div className="big-spacer-left big-spacer-right branch-baseline-setting-modal"> | |||
<div className="display-flex-row big-spacer-bottom" role="radiogroup"> | |||
<BaselineSettingPreviousVersion | |||
disabled={!overrideGeneralSetting} | |||
onSelect={props.onSelectSetting} | |||
selected={selected === 'PREVIOUS_VERSION'} | |||
selected={overrideGeneralSetting && selected === 'PREVIOUS_VERSION'} | |||
/> | |||
<BaselineSettingDays | |||
days={days} | |||
disabled={!overrideGeneralSetting} | |||
isChanged={isChanged} | |||
isValid={isValid} | |||
onChangeDays={props.onSelectDays} | |||
onSelect={props.onSelectSetting} | |||
selected={selected === 'NUMBER_OF_DAYS'} | |||
selected={overrideGeneralSetting && selected === 'NUMBER_OF_DAYS'} | |||
/> | |||
{!branchesEnabled && ( | |||
<BaselineSettingAnalysis | |||
disabled={!overrideGeneralSetting} | |||
onSelect={props.onSelectSetting} | |||
selected={selected === 'SPECIFIC_ANALYSIS'} | |||
selected={overrideGeneralSetting && selected === 'SPECIFIC_ANALYSIS'} | |||
/> | |||
)} | |||
</div> |
@@ -17,12 +17,23 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
.project-baseline { | |||
padding: calc(4 * var(--gridSize)); | |||
} | |||
.project-baseline-selector > .branch-baseline-setting-modal { | |||
max-height: 60vh; | |||
padding-top: 2px; | |||
} | |||
.project-baseline-selector .general-setting { | |||
margin-left: 7px; | |||
} | |||
.branch-baseline-selector > hr { | |||
margin: 0 calc(-4 * var(--gridSize)) calc(4 * var(--gridSize)); | |||
} | |||
.branch-baseline-setting-modal { | |||
display: flex; | |||
flex-direction: column; | |||
@@ -44,7 +55,7 @@ | |||
} | |||
.branch-analysis-list > ul { | |||
padding-top: 52px; | |||
padding-top: 18px; | |||
} | |||
.branch-analysis-date { | |||
@@ -90,6 +101,10 @@ | |||
background-color: white; | |||
} | |||
.branch-analysis-version-badge.sticky + .branch-analysis-days-list { | |||
padding-top: 36px; | |||
} | |||
.branch-analysis-version-badge.sticky, | |||
.branch-analysis-version-badge.first { | |||
position: absolute; |
@@ -48,15 +48,30 @@ export function validateSetting(state: { | |||
currentSettingValue?: string; | |||
days: string; | |||
selected?: T.NewCodePeriodSettingType; | |||
overrideGeneralSetting?: boolean; | |||
}) { | |||
const { analysis = '', currentSetting, currentSettingValue, days, selected } = state; | |||
const { | |||
analysis = '', | |||
currentSetting, | |||
currentSettingValue, | |||
days, | |||
selected, | |||
overrideGeneralSetting | |||
} = state; | |||
const isChanged = | |||
selected !== currentSetting || | |||
(selected === 'NUMBER_OF_DAYS' && days !== currentSettingValue) || | |||
(selected === 'SPECIFIC_ANALYSIS' && analysis !== currentSettingValue); | |||
let isChanged; | |||
if (!currentSetting && overrideGeneralSetting !== undefined) { | |||
isChanged = overrideGeneralSetting; | |||
} else { | |||
isChanged = | |||
overrideGeneralSetting === false || | |||
selected !== currentSetting || | |||
(selected === 'NUMBER_OF_DAYS' && days !== currentSettingValue) || | |||
(selected === 'SPECIFIC_ANALYSIS' && analysis !== currentSettingValue); | |||
} | |||
const isValid = | |||
overrideGeneralSetting === false || | |||
selected === 'PREVIOUS_VERSION' || | |||
(selected === 'SPECIFIC_ANALYSIS' && analysis.length > 0) || | |||
(selected === 'NUMBER_OF_DAYS' && validateDays(days)); |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
@@ -84,6 +84,13 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { | |||
this.setState({ selected, success: false }); | |||
}; | |||
onCancel = () => { | |||
this.setState(({ currentSetting, currentSettingValue, days }) => ({ | |||
selected: currentSetting, | |||
days: currentSetting === 'NUMBER_OF_DAYS' ? String(currentSettingValue) : days | |||
})); | |||
}; | |||
onSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => { | |||
e.preventDefault(); | |||
@@ -191,6 +198,9 @@ export default class NewCodePeriod extends React.PureComponent<{}, State> { | |||
<SubmitButton disabled={saving || !isValid}> | |||
{translate('save')} | |||
</SubmitButton> | |||
<ResetButtonLink className="spacer-left" onClick={this.onCancel}> | |||
{translate('cancel')} | |||
</ResetButtonLink> | |||
</div> | |||
)} | |||
{!saving && !loading && success && ( |
@@ -85,15 +85,12 @@ describe('getPeriodLabel', () => { | |||
it('should handle SPECIFIC_ANALYSIS', () => { | |||
expect( | |||
getPeriodLabel( | |||
mockPeriod({ | |||
mode: 'SPECIFIC_ANALYSIS', | |||
parameter: 'should be overriden' | |||
}), | |||
formatter | |||
) | |||
).toBe('overview.period.specific_analysis.2019-04-23T02:12:32+0100'); | |||
expect(formatter).toBeCalled(); | |||
getPeriodLabel(mockPeriod({ mode: 'SPECIFIC_ANALYSIS', modeParam: 'A658678DE' }), formatter) | |||
).toBe('overview.period.specific_analysis.A658678DE'); | |||
expect(getPeriodLabel(mockPeriod({ mode: 'SPECIFIC_ANALYSIS' }), formatter)).toBe( | |||
'overview.period.specific_analysis.2019-04-23T02:12:32+0100' | |||
); | |||
expect(formatter).toBeCalledTimes(1); | |||
}); | |||
it('should handle PREVIOUS_VERSION', () => { |
@@ -43,7 +43,7 @@ export function getPeriodLabel( | |||
switch (period.mode) { | |||
case 'SPECIFIC_ANALYSIS': | |||
parameter = dateFormatter(period.date); | |||
parameter = parameter || dateFormatter(period.date); | |||
break; | |||
case 'PREVIOUS_VERSION': | |||
parameter = parameter || dateFormatter(period.date); |
@@ -543,10 +543,10 @@ project_baseline.page.description=Use this page to manage the New Code Period of | |||
project_baseline.page.description.link=Learn More | |||
project_baseline.page.description2=You can adjust this setting globally in {link} | |||
project_baseline.page.description2.link=General Settings | |||
project_baseline.default_setting=Project default setting | |||
project_baseline.default_setting.description=This setting is the default for all branches of the project | |||
project_baseline.general_setting=General setting | |||
project_baseline.reset_to_general=Reset to general setting | |||
project_baseline.default_setting=Project setting | |||
project_baseline.general_setting=Use the general setting | |||
project_baseline.specific_setting=Define a specific setting for this project | |||
project_baseline.configure_branches=Set a specific setting for a branch | |||
baseline.previous_version=Previous version | |||
baseline.previous_version.description=The New Code Period will begin with the analysis following the previous version. |