@@ -38,9 +38,11 @@ interface Props { | |||
interface State { | |||
formData: ProjectAlmBinding; | |||
instances: AlmSettingsInstance[]; | |||
isChanged: boolean; | |||
isConfigured: boolean; | |||
isValid: boolean; | |||
loading: boolean; | |||
originalData?: ProjectAlmBinding; | |||
orignalData?: ProjectAlmBinding; | |||
saving: boolean; | |||
success: boolean; | |||
} | |||
@@ -59,6 +61,8 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat | |||
state: State = { | |||
formData: { key: '' }, | |||
instances: [], | |||
isChanged: false, | |||
isConfigured: false, | |||
isValid: false, | |||
loading: true, | |||
saving: false, | |||
@@ -84,9 +88,11 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat | |||
return { | |||
formData: newFormData, | |||
instances: instances || [], | |||
isChanged: false, | |||
isConfigured: !!originalData, | |||
isValid: this.validateForm(newFormData), | |||
loading: false, | |||
originalData | |||
orignalData: newFormData | |||
}; | |||
}); | |||
} | |||
@@ -125,7 +131,8 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat | |||
repository: '', | |||
slug: '' | |||
}, | |||
originalData: undefined, | |||
isChanged: false, | |||
isConfigured: false, | |||
saving: false, | |||
success: true | |||
}); | |||
@@ -161,14 +168,20 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat | |||
}); | |||
} | |||
case AlmKeys.GitHub: { | |||
const repository = almSpecificFields && almSpecificFields.repository; | |||
const repository = almSpecificFields?.repository; | |||
// By default it must remain true. | |||
const summaryCommentEnabled = | |||
almSpecificFields?.summaryCommentEnabled === undefined | |||
? true | |||
: almSpecificFields?.summaryCommentEnabled; | |||
if (!repository) { | |||
return Promise.reject(); | |||
} | |||
return setProjectGithubBinding({ | |||
almSetting, | |||
project, | |||
repository | |||
repository, | |||
summaryCommentEnabled | |||
}); | |||
} | |||
@@ -198,23 +211,38 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat | |||
return; | |||
} | |||
if (key) { | |||
this.submitProjectAlmBinding(selected.alm, key, additionalFields) | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ | |||
saving: false, | |||
success: true | |||
}); | |||
} | |||
}) | |||
.then(this.fetchDefinitions) | |||
.catch(this.catchError); | |||
} | |||
this.submitProjectAlmBinding(selected.alm, key, additionalFields) | |||
.then(() => { | |||
if (this.mounted) { | |||
this.setState({ | |||
saving: false, | |||
success: true | |||
}); | |||
} | |||
}) | |||
.then(this.fetchDefinitions) | |||
.catch(this.catchError); | |||
}; | |||
handleFieldChange = (id: keyof ProjectAlmBinding, value: string) => { | |||
this.setState(({ formData }) => { | |||
isDataSame( | |||
{ key, repository = '', slug = '', summaryCommentEnabled = false }: ProjectAlmBinding, | |||
{ | |||
key: oKey = '', | |||
repository: oRepository = '', | |||
slug: oSlug = '', | |||
summaryCommentEnabled: osummaryCommentEnabled = false | |||
}: ProjectAlmBinding | |||
) { | |||
return ( | |||
key === oKey && | |||
repository === oRepository && | |||
slug === oSlug && | |||
summaryCommentEnabled === osummaryCommentEnabled | |||
); | |||
} | |||
handleFieldChange = (id: keyof ProjectAlmBinding, value: string | boolean) => { | |||
this.setState(({ formData, orignalData }) => { | |||
const newFormData = { | |||
...formData, | |||
[id]: value | |||
@@ -222,6 +250,7 @@ export default class PRDecorationBinding extends React.PureComponent<Props, Stat | |||
return { | |||
formData: newFormData, | |||
isValid: this.validateForm(newFormData), | |||
isChanged: !this.isDataSame(newFormData, orignalData || { key: '' }), | |||
success: false | |||
}; | |||
}); |
@@ -28,20 +28,34 @@ import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { AlmKeys, AlmSettingsInstance, ProjectAlmBinding } from '../../../../types/alm-settings'; | |||
import InputForBoolean from '../inputs/InputForBoolean'; | |||
export interface PRDecorationBindingRendererProps { | |||
formData: ProjectAlmBinding; | |||
instances: AlmSettingsInstance[]; | |||
isChanged: boolean; | |||
isConfigured: boolean; | |||
isValid: boolean; | |||
loading: boolean; | |||
onFieldChange: (id: keyof ProjectAlmBinding, value: string) => void; | |||
onFieldChange: (id: keyof ProjectAlmBinding, value: string | boolean) => void; | |||
onReset: () => void; | |||
onSubmit: () => void; | |||
originalData?: ProjectAlmBinding; | |||
saving: boolean; | |||
success: boolean; | |||
} | |||
interface LabelProps { | |||
help?: boolean; | |||
helpParams?: T.Dict<string | JSX.Element>; | |||
id: string; | |||
optional?: boolean; | |||
} | |||
interface CommonFieldProps extends LabelProps { | |||
onFieldChange: (id: keyof ProjectAlmBinding, value: string | boolean) => void; | |||
propKey: keyof ProjectAlmBinding; | |||
} | |||
function optionRenderer(instance: AlmSettingsInstance) { | |||
return instance.url ? ( | |||
<> | |||
@@ -53,35 +67,57 @@ function optionRenderer(instance: AlmSettingsInstance) { | |||
); | |||
} | |||
function renderField(props: { | |||
help?: boolean; | |||
helpParams?: { [key: string]: string | JSX.Element }; | |||
id: string; | |||
onFieldChange: (id: keyof ProjectAlmBinding, value: string) => void; | |||
optional?: boolean; | |||
propKey: keyof ProjectAlmBinding; | |||
value: string; | |||
}) { | |||
const { help, helpParams, id, propKey, optional, value, onFieldChange } = props; | |||
function renderLabel(props: LabelProps) { | |||
const { help, helpParams, optional, id } = props; | |||
return ( | |||
<label className="display-flex-center" htmlFor={id}> | |||
{translate('settings.pr_decoration.binding.form', id)} | |||
{!optional && <em className="mandatory">*</em>} | |||
{help && ( | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<FormattedMessage | |||
defaultMessage={translate('settings.pr_decoration.binding.form', id, 'help')} | |||
id={`settings.pr_decoration.binding.form.${id}.help`} | |||
values={helpParams} | |||
/> | |||
} | |||
placement="right" | |||
/> | |||
)} | |||
</label> | |||
); | |||
} | |||
function renderBooleanField( | |||
props: Omit<CommonFieldProps, 'optional'> & { | |||
value: boolean; | |||
} | |||
) { | |||
const { id, value, onFieldChange, propKey } = props; | |||
return ( | |||
<div className="form-field"> | |||
<label className="display-flex-center" htmlFor={id}> | |||
{translate('settings.pr_decoration.binding.form', id)} | |||
{!optional && <em className="mandatory">*</em>} | |||
{help && ( | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<FormattedMessage | |||
defaultMessage={translate('settings.pr_decoration.binding.form', id, 'help')} | |||
id={`settings.pr_decoration.binding.form.${id}.help`} | |||
values={helpParams} | |||
/> | |||
} | |||
placement="right" | |||
/> | |||
)} | |||
</label> | |||
{renderLabel({ ...props, optional: true })} | |||
<InputForBoolean | |||
isDefault={true} | |||
name={id} | |||
onChange={v => onFieldChange(propKey, v)} | |||
value={value} | |||
/> | |||
</div> | |||
); | |||
} | |||
function renderField( | |||
props: CommonFieldProps & { | |||
value: string; | |||
} | |||
) { | |||
const { id, propKey, value, onFieldChange } = props; | |||
return ( | |||
<div className="form-field"> | |||
{renderLabel(props)} | |||
<input | |||
className="input-super-large" | |||
id={id} | |||
@@ -95,20 +131,14 @@ function renderField(props: { | |||
); | |||
} | |||
function isDataSame( | |||
{ key, repository = '', slug = '' }: ProjectAlmBinding, | |||
{ key: oKey = '', repository: oRepository = '', slug: oSlug = '' }: ProjectAlmBinding | |||
) { | |||
return key === oKey && repository === oRepository && slug === oSlug; | |||
} | |||
export default function PRDecorationBindingRenderer(props: PRDecorationBindingRendererProps) { | |||
const { | |||
formData: { key, repository, slug }, | |||
formData: { key, repository, slug, summaryCommentEnabled }, | |||
instances, | |||
isChanged, | |||
isConfigured, | |||
isValid, | |||
loading, | |||
originalData, | |||
saving, | |||
success | |||
} = props; | |||
@@ -140,8 +170,6 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe | |||
const selected = key && instances.find(i => i.key === key); | |||
const alm = selected && selected.alm; | |||
const isChanged = !isDataSame({ key, repository, slug }, originalData || { key: '' }); | |||
return ( | |||
<div> | |||
<header className="page-header"> | |||
@@ -218,15 +246,25 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe | |||
</> | |||
)} | |||
{alm === AlmKeys.GitHub && | |||
renderField({ | |||
help: true, | |||
helpParams: { example: 'SonarSource/sonarqube' }, | |||
id: 'github.repository', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'repository', | |||
value: repository || '' | |||
})} | |||
{alm === AlmKeys.GitHub && ( | |||
<> | |||
{renderField({ | |||
help: true, | |||
helpParams: { example: 'SonarSource/sonarqube' }, | |||
id: 'github.repository', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'repository', | |||
value: repository || '' | |||
})} | |||
{renderBooleanField({ | |||
help: true, | |||
id: 'github.summary_comment_setting', | |||
onFieldChange: props.onFieldChange, | |||
propKey: 'summaryCommentEnabled', | |||
value: summaryCommentEnabled === undefined ? true : summaryCommentEnabled | |||
})} | |||
</> | |||
)} | |||
{alm === AlmKeys.GitLab && | |||
renderField({ | |||
@@ -245,7 +283,7 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe | |||
<span data-test="project-settings__alm-save">{translate('save')}</span> | |||
</SubmitButton> | |||
)} | |||
{originalData && ( | |||
{isConfigured && ( | |||
<Button className="spacer-right" onClick={props.onReset}> | |||
<span data-test="project-settings__alm-reset">{translate('reset_verb')}</span> | |||
</Button> |
@@ -66,7 +66,7 @@ it('should fill selects and fill formdata', async () => { | |||
expect(wrapper.state().loading).toBe(false); | |||
expect(wrapper.state().formData).toEqual(formdata); | |||
expect(wrapper.state().originalData).toEqual(formdata); | |||
expect(wrapper.state().isChanged).toBe(false); | |||
}); | |||
it('should handle reset', async () => { | |||
@@ -84,7 +84,7 @@ it('should handle reset', async () => { | |||
expect(deleteProjectAlmBinding).toBeCalledWith(PROJECT_KEY); | |||
expect(wrapper.state().formData).toEqual({ key: '', repository: '', slug: '' }); | |||
expect(wrapper.state().originalData).toBeUndefined(); | |||
expect(wrapper.state().isChanged).toBe(false); | |||
}); | |||
describe('handleSubmit', () => { | |||
@@ -99,14 +99,19 @@ describe('handleSubmit', () => { | |||
await waitAndUpdate(wrapper); | |||
const githubKey = 'github'; | |||
const repository = 'repo/path'; | |||
wrapper.setState({ formData: { key: githubKey, repository }, instances }); | |||
const summaryCommentEnabled = true; | |||
wrapper.setState({ | |||
formData: { key: githubKey, repository, summaryCommentEnabled }, | |||
instances | |||
}); | |||
wrapper.instance().handleSubmit(); | |||
await waitAndUpdate(wrapper); | |||
expect(setProjectGithubBinding).toBeCalledWith({ | |||
almSetting: githubKey, | |||
project: PROJECT_KEY, | |||
repository | |||
repository, | |||
summaryCommentEnabled | |||
}); | |||
expect(wrapper.state().success).toBe(true); | |||
}); | |||
@@ -146,23 +151,31 @@ describe('handleSubmit', () => { | |||
}); | |||
}); | |||
it('should handle failures gracefully', async () => { | |||
(getProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status: 500 }); | |||
(setProjectGithubBinding as jest.Mock).mockRejectedValueOnce({ status: 500 }); | |||
(deleteProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status: 500 }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ | |||
formData: { | |||
describe.each([[500], [404]])('For status %i', status => { | |||
it('should handle failures gracefully', async () => { | |||
const newFormData = { | |||
key: 'whatever', | |||
repository: 'something/else' | |||
} | |||
}); | |||
}; | |||
wrapper.instance().handleSubmit(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleReset(); | |||
(getProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status }); | |||
(setProjectGithubBinding as jest.Mock).mockRejectedValueOnce({ status }); | |||
(deleteProjectAlmBinding as jest.Mock).mockRejectedValueOnce({ status }); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ | |||
formData: newFormData, | |||
orignalData: undefined | |||
}); | |||
wrapper.instance().handleSubmit(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.instance().state.orignalData).toBeUndefined(); | |||
wrapper.instance().handleReset(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.instance().state.formData).toEqual(newFormData); | |||
}); | |||
}); | |||
it('should handle field changes', async () => { | |||
@@ -189,6 +202,48 @@ it('should handle field changes', async () => { | |||
key: 'instance2', | |||
repository | |||
}); | |||
wrapper.instance().handleFieldChange('summaryCommentEnabled', true); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().formData).toEqual({ | |||
key: 'instance2', | |||
repository, | |||
summaryCommentEnabled: true | |||
}); | |||
}); | |||
it('should reject submit github settings', async () => { | |||
const wrapper = shallowRender(); | |||
expect.assertions(1); | |||
await expect( | |||
wrapper.instance().submitProjectAlmBinding(AlmKeys.GitHub, 'github-binding', {}) | |||
).rejects.toBe(undefined); | |||
}); | |||
it('should accept submit github settings', async () => { | |||
(setProjectGithubBinding as jest.Mock).mockRestore(); | |||
const wrapper = shallowRender(); | |||
await wrapper | |||
.instance() | |||
.submitProjectAlmBinding(AlmKeys.GitHub, 'github-binding', { repository: 'foo' }); | |||
expect(setProjectGithubBinding).toHaveBeenCalledWith({ | |||
almSetting: 'github-binding', | |||
project: PROJECT_KEY, | |||
repository: 'foo', | |||
summaryCommentEnabled: true | |||
}); | |||
await wrapper.instance().submitProjectAlmBinding(AlmKeys.GitHub, 'github-binding', { | |||
repository: 'foo', | |||
summaryCommentEnabled: true | |||
}); | |||
expect(setProjectGithubBinding).toHaveBeenCalledWith({ | |||
almSetting: 'github-binding', | |||
project: PROJECT_KEY, | |||
repository: 'foo', | |||
summaryCommentEnabled: true | |||
}); | |||
}); | |||
it('should validate form', async () => { |
@@ -84,12 +84,10 @@ it('should render multiple instances correctly', () => { | |||
key: 'i1', | |||
repository: 'account/repo' | |||
}, | |||
isChanged: false, | |||
isConfigured: true, | |||
instances, | |||
loading: false, | |||
originalData: { | |||
key: 'i1', | |||
repository: 'account/repo' | |||
} | |||
loading: false | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
@@ -137,6 +135,8 @@ it('should render optional fields correctly', () => { | |||
formData: { | |||
key: 'key' | |||
}, | |||
isChanged: true, | |||
isConfigured: false, | |||
instances: [{ key: 'key', url: 'http://example.com', alm: AlmKeys.GitLab }], | |||
loading: false | |||
}) | |||
@@ -151,12 +151,13 @@ function shallowRender(props: Partial<PRDecorationBindingRendererProps> = {}) { | |||
repository: '' | |||
}} | |||
instances={[]} | |||
isChanged={false} | |||
isConfigured={false} | |||
isValid={false} | |||
loading={true} | |||
onFieldChange={jest.fn()} | |||
onReset={jest.fn()} | |||
onSubmit={jest.fn()} | |||
originalData={undefined} | |||
saving={false} | |||
success={false} | |||
{...props} |
@@ -8,6 +8,8 @@ exports[`should render correctly 1`] = ` | |||
} | |||
} | |||
instances={Array []} | |||
isChanged={false} | |||
isConfigured={false} | |||
isValid={false} | |||
loading={true} | |||
onFieldChange={[Function]} |
@@ -458,6 +458,33 @@ exports[`should render multiple instances correctly 2`] = ` | |||
value="account/repo" | |||
/> | |||
</div> | |||
<div | |||
className="form-field" | |||
> | |||
<label | |||
className="display-flex-center" | |||
htmlFor="github.summary_comment_setting" | |||
> | |||
settings.pr_decoration.binding.form.github.summary_comment_setting | |||
<HelpTooltip | |||
className="spacer-left" | |||
overlay={ | |||
<FormattedMessage | |||
defaultMessage="settings.pr_decoration.binding.form.github.summary_comment_setting.help" | |||
id="settings.pr_decoration.binding.form.github.summary_comment_setting.help" | |||
values={Object {}} | |||
/> | |||
} | |||
placement="right" | |||
/> | |||
</label> | |||
<InputForBoolean | |||
isDefault={true} | |||
name="github.summary_comment_setting" | |||
onChange={[Function]} | |||
value={true} | |||
/> | |||
</div> | |||
<div | |||
className="display-flex-center" | |||
> |
@@ -52,6 +52,7 @@ export interface ProjectAlmBinding { | |||
key: string; | |||
repository?: string; | |||
slug?: string; | |||
summaryCommentEnabled?: boolean; | |||
} | |||
export interface AzureProjectAlmBinding { | |||
@@ -70,6 +71,7 @@ export interface GithubProjectAlmBinding { | |||
almSetting: string; | |||
project: string; | |||
repository: string; | |||
summaryCommentEnabled: boolean; | |||
} | |||
export interface GitlabProjectAlmBinding { |
@@ -1076,6 +1076,8 @@ settings.pr_decoration.binding.form.url=Project location | |||
settings.pr_decoration.binding.form.name=Configuration name | |||
settings.pr_decoration.binding.form.github.repository=Repository identifier | |||
settings.pr_decoration.binding.form.github.repository.help=The path of your repository URL. Example: {example} | |||
settings.pr_decoration.binding.form.github.summary_comment_setting=Enable analysis summary under the GitHub Conversation tab | |||
settings.pr_decoration.binding.form.github.summary_comment_setting.help=When enabled, Pull Request analysis summary is displayed under the GitHub Conversation tab. Notifications may be sent by GitHub depending on your settings. | |||
settings.pr_decoration.binding.form.bitbucket.repository=Project Key | |||
settings.pr_decoration.binding.form.bitbucket.repository.help=The project key is part of your Bitbucket Server repository URL. Example: ({example}) | |||
settings.pr_decoration.binding.form.bitbucket.slug=Repository SLUG |