interface State {
formData: ProjectAlmBinding;
instances: AlmSettingsInstance[];
+ isChanged: boolean;
+ isConfigured: boolean;
isValid: boolean;
loading: boolean;
- originalData?: ProjectAlmBinding;
+ orignalData?: ProjectAlmBinding;
saving: boolean;
success: boolean;
}
state: State = {
formData: { key: '' },
instances: [],
+ isChanged: false,
+ isConfigured: false,
isValid: false,
loading: true,
saving: false,
return {
formData: newFormData,
instances: instances || [],
+ isChanged: false,
+ isConfigured: !!originalData,
isValid: this.validateForm(newFormData),
loading: false,
- originalData
+ orignalData: newFormData
};
});
}
repository: '',
slug: ''
},
- originalData: undefined,
+ isChanged: false,
+ isConfigured: false,
saving: false,
success: true
});
});
}
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
});
}
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
return {
formData: newFormData,
isValid: this.validateForm(newFormData),
+ isChanged: !this.isDataSame(newFormData, orignalData || { key: '' }),
success: false
};
});
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 ? (
<>
);
}
-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}
);
}
-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;
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">
</>
)}
- {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({
<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>
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 () => {
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', () => {
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);
});
});
});
-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 () => {
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 () => {
key: 'i1',
repository: 'account/repo'
},
+ isChanged: false,
+ isConfigured: true,
instances,
- loading: false,
- originalData: {
- key: 'i1',
- repository: 'account/repo'
- }
+ loading: false
})
).toMatchSnapshot();
});
formData: {
key: 'key'
},
+ isChanged: true,
+ isConfigured: false,
instances: [{ key: 'key', url: 'http://example.com', alm: AlmKeys.GitLab }],
loading: false
})
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}
}
}
instances={Array []}
+ isChanged={false}
+ isConfigured={false}
isValid={false}
loading={true}
onFieldChange={[Function]}
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"
>
key: string;
repository?: string;
slug?: string;
+ summaryCommentEnabled?: boolean;
}
export interface AzureProjectAlmBinding {
almSetting: string;
project: string;
repository: string;
+ summaryCommentEnabled: boolean;
}
export interface GitlabProjectAlmBinding {
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