--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info 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 { Profile } from '../types';
+import { createQualityProfile, changeProfileParent } from '../../../api/quality-profiles';
+import Modal from '../../../components/controls/Modal';
+import { SubmitButton, ResetButtonLink } from '../../../components/ui/buttons';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+
+interface Props {
+ onClose: () => void;
+ onExtend: (name: string) => void;
+ organization: string | null;
+ profile: Profile;
+}
+
+interface State {
+ loading: boolean;
+ name?: string;
+}
+
+type ValidState = State & Required<Pick<State, 'name'>>;
+
+export default class ExtendProfileForm extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ canSubmit = (state: State): state is ValidState => {
+ return Boolean(state.name && state.name.length);
+ };
+
+ handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+ this.setState({ name: event.currentTarget.value });
+ };
+
+ handleFormSubmit = async () => {
+ if (this.canSubmit(this.state)) {
+ const { organization, profile: parentProfile } = this.props;
+ const { name } = this.state;
+
+ const data = new FormData();
+
+ data.append('language', parentProfile.language);
+ data.append('name', name);
+
+ if (organization) {
+ data.append('organization', organization);
+ }
+
+ this.setState({ loading: true });
+
+ try {
+ const { profile: newProfile } = await createQualityProfile(data);
+ await changeProfileParent(newProfile.key, parentProfile.key);
+ this.props.onExtend(newProfile.name);
+ } finally {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ }
+ };
+
+ render() {
+ const { profile } = this.props;
+ const header = translateWithParameters(
+ 'quality_profiles.extend_x_title',
+ profile.name,
+ profile.languageName
+ );
+
+ return (
+ <Modal contentLabel={header} onRequestClose={this.props.onClose}>
+ <form>
+ <div className="modal-head">
+ <h2>{header}</h2>
+ </div>
+ <div className="modal-body">
+ <div className="modal-field">
+ <label htmlFor="extend-profile-name">
+ {translate('quality_profiles.copy_new_name')}
+ <em className="mandatory">*</em>
+ </label>
+ <input
+ autoFocus={true}
+ id="extend-profile-name"
+ maxLength={100}
+ name="name"
+ onChange={this.handleNameChange}
+ required={true}
+ size={50}
+ type="text"
+ value={this.state.name ? this.state.name : ''}
+ />
+ </div>
+ </div>
+ <div className="modal-foot">
+ <DeferredSpinner className="spacer-right" loading={this.state.loading} />
+ <SubmitButton
+ disabled={this.state.loading || !this.canSubmit(this.state)}
+ id="extend-profile-submit"
+ onClick={this.handleFormSubmit}>
+ {translate('copy')}
+ </SubmitButton>
+ <ResetButtonLink id="extend-profile-cancel" onClick={this.props.onClose}>
+ {translate('cancel')}
+ </ResetButtonLink>
+ </div>
+ </form>
+ </Modal>
+ );
+ }
+}
import RenameProfileForm from './RenameProfileForm';
import CopyProfileForm from './CopyProfileForm';
import DeleteProfileForm from './DeleteProfileForm';
+import ExtendProfileForm from './ExtendProfileForm';
import { translate } from '../../../helpers/l10n';
import { getRulesUrl } from '../../../helpers/urls';
import { setDefaultProfile } from '../../../api/quality-profiles';
interface State {
copyFormOpen: boolean;
+ extendFormOpen: boolean;
deleteFormOpen: boolean;
renameFormOpen: boolean;
}
export class ProfileActions extends React.PureComponent<Props, State> {
state: State = {
copyFormOpen: false,
+ extendFormOpen: false,
deleteFormOpen: false,
renameFormOpen: false
};
+ closeCopyForm = () => {
+ this.setState({ copyFormOpen: false });
+ };
+
+ closeDeleteForm = () => {
+ this.setState({ deleteFormOpen: false });
+ };
+
+ closeExtendForm = () => {
+ this.setState({ extendFormOpen: false });
+ };
+
+ closeRenameForm = () => {
+ this.setState({ renameFormOpen: false });
+ };
+
+ handleCopyClick = () => {
+ this.setState({ copyFormOpen: true });
+ };
+
+ handleDeleteClick = () => {
+ this.setState({ deleteFormOpen: true });
+ };
+
+ handleExtendClick = () => {
+ this.setState({ extendFormOpen: true });
+ };
+
handleRenameClick = () => {
this.setState({ renameFormOpen: true });
};
+ handleProfileCopy = (name: string) => {
+ this.closeCopyForm();
+ this.navigateToNewProfile(name);
+ };
+
+ handleProfileDelete = () => {
+ this.props.router.replace(getProfilesPath(this.props.organization));
+ this.props.updateProfiles();
+ };
+
+ handleProfileExtend = (name: string) => {
+ this.closeExtendForm();
+ this.navigateToNewProfile(name);
+ };
+
handleProfileRename = (name: string) => {
this.closeRenameForm();
this.props.updateProfiles().then(
);
};
- closeRenameForm = () => {
- this.setState({ renameFormOpen: false });
- };
-
- handleCopyClick = () => {
- this.setState({ copyFormOpen: true });
+ handleSetDefaultClick = () => {
+ setDefaultProfile(this.props.profile.key).then(this.props.updateProfiles, () => {});
};
- handleProfileCopy = (name: string) => {
- this.closeCopyForm();
+ navigateToNewProfile = (name: string) => {
this.props.updateProfiles().then(
() => {
this.props.router.push(
);
};
- closeCopyForm = () => {
- this.setState({ copyFormOpen: false });
- };
-
- handleSetDefaultClick = () => {
- setDefaultProfile(this.props.profile.key).then(this.props.updateProfiles, () => {});
- };
-
- handleDeleteClick = () => {
- this.setState({ deleteFormOpen: true });
- };
-
- handleProfileDelete = () => {
- this.props.router.replace(getProfilesPath(this.props.organization));
- this.props.updateProfiles();
- };
-
- closeDeleteForm = () => {
- this.setState({ deleteFormOpen: false });
- };
-
render() {
const { profile } = this.props;
const { actions = {} } = profile;
</ActionsDropdownItem>
{actions.copy && (
- <ActionsDropdownItem id="quality-profile-copy" onClick={this.handleCopyClick}>
- {translate('copy')}
- </ActionsDropdownItem>
+ <>
+ <ActionsDropdownItem id="quality-profile-copy" onClick={this.handleCopyClick}>
+ {translate('copy')}
+ </ActionsDropdownItem>
+
+ <ActionsDropdownItem id="quality-profile-extend" onClick={this.handleExtendClick}>
+ {translate('extend')}
+ </ActionsDropdownItem>
+ </>
)}
{actions.edit && (
/>
)}
+ {this.state.extendFormOpen && (
+ <ExtendProfileForm
+ onClose={this.closeExtendForm}
+ onExtend={this.handleProfileExtend}
+ organization={this.props.organization}
+ profile={profile}
+ />
+ )}
+
{this.state.deleteFormOpen && (
<DeleteProfileForm
onClose={this.closeDeleteForm}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info 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 ExtendProfileForm from '../ExtendProfileForm';
+import { createQualityProfile, changeProfileParent } from '../../../../api/quality-profiles';
+import { mockQualityProfile } from '../../testUtils';
+import { click } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/quality-profiles', () => ({
+ createQualityProfile: jest.fn().mockResolvedValue({ profile: { key: 'new-profile' } }),
+ changeProfileParent: jest.fn().mockResolvedValue(true)
+}));
+
+it('should render correctly', () => {
+ const wrapper = shallowRender();
+ expect(wrapper).toMatchSnapshot();
+});
+
+it.only('should correctly create a new profile and extend the existing one', async () => {
+ const profile = mockQualityProfile();
+ const organization = 'org';
+ const name = 'New name';
+ const wrapper = shallowRender({ organization, profile });
+
+ click(wrapper.find('SubmitButton'));
+ expect(createQualityProfile).not.toHaveBeenCalled();
+ expect(changeProfileParent).not.toHaveBeenCalled();
+
+ wrapper.setState({ name }).update();
+ click(wrapper.find('SubmitButton'));
+ await Promise.resolve(setImmediate);
+
+ const data = new FormData();
+ data.append('language', profile.language);
+ data.append('name', name);
+ data.append('organization', organization);
+ expect(createQualityProfile).toHaveBeenCalledWith(data);
+ expect(changeProfileParent).toHaveBeenCalledWith('new-profile', profile.key);
+});
+
+function shallowRender(props: Partial<ExtendProfileForm['props']> = {}) {
+ return shallow(
+ <ExtendProfileForm
+ onClose={jest.fn()}
+ onExtend={jest.fn()}
+ organization="foo"
+ profile={mockQualityProfile()}
+ {...props}
+ />
+ );
+}
import * as React from 'react';
import { shallow } from 'enzyme';
import { ProfileActions } from '../ProfileActions';
-import { click, waitAndUpdate } from '../../../../helpers/testUtils';
+import { click, waitAndUpdate, mockRouter } from '../../../../helpers/testUtils';
import { mockQualityProfile } from '../../testUtils';
const PROFILE = mockQualityProfile({
});
it('renders with no permissions', () => {
- expect(
- shallow(
- <ProfileActions
- organization="org"
- profile={PROFILE}
- router={{ push: jest.fn(), replace: jest.fn() }}
- updateProfiles={jest.fn()}
- />
- )
- ).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
});
it('renders with permission to edit only', () => {
- expect(
- shallow(
- <ProfileActions
- organization="org"
- profile={{ ...PROFILE, actions: { edit: true } }}
- router={{ push: jest.fn(), replace: jest.fn() }}
- updateProfiles={jest.fn()}
- />
- )
- ).toMatchSnapshot();
+ expect(shallowRender({ profile: { ...PROFILE, actions: { edit: true } } })).toMatchSnapshot();
});
it('renders with all permissions', () => {
expect(
- shallow(
- <ProfileActions
- organization="org"
- profile={{
- ...PROFILE,
- actions: {
- copy: true,
- edit: true,
- delete: true,
- setAsDefault: true,
- associateProjects: true
- }
- }}
- router={{ push: jest.fn(), replace: jest.fn() }}
- updateProfiles={jest.fn()}
- />
- )
+ shallowRender({
+ profile: {
+ ...PROFILE,
+ actions: {
+ copy: true,
+ edit: true,
+ delete: true,
+ setAsDefault: true,
+ associateProjects: true
+ }
+ }
+ })
).toMatchSnapshot();
});
it('should copy profile', async () => {
+ const name = 'new-name';
const updateProfiles = jest.fn(() => Promise.resolve());
const push = jest.fn();
- const wrapper = shallow(
- <ProfileActions
- organization="org"
- profile={{ ...PROFILE, actions: { copy: true } }}
- router={{ push, replace: jest.fn() }}
- updateProfiles={updateProfiles}
- />
- );
+ const wrapper = shallowRender({
+ profile: { ...PROFILE, actions: { copy: true } },
+ router: { push, replace: jest.fn() },
+ updateProfiles
+ });
click(wrapper.find('[id="quality-profile-copy"]'));
expect(wrapper.find('CopyProfileForm').exists()).toBe(true);
- wrapper.find('CopyProfileForm').prop<Function>('onCopy')('new-name');
+ wrapper.find('CopyProfileForm').prop<Function>('onCopy')(name);
expect(updateProfiles).toBeCalled();
await waitAndUpdate(wrapper);
expect(push).toBeCalledWith({
pathname: '/organizations/org/quality_profiles/show',
- query: { language: 'js', name: 'new-name' }
+ query: { language: 'js', name }
});
expect(wrapper.find('CopyProfileForm').exists()).toBe(false);
});
+
+it('should extend profile', async () => {
+ const name = 'new-name';
+ const updateProfiles = jest.fn(() => Promise.resolve());
+ const push = jest.fn();
+ const wrapper = shallowRender({
+ profile: { ...PROFILE, actions: { copy: true } },
+ router: { push, replace: jest.fn() },
+ updateProfiles
+ });
+
+ click(wrapper.find('[id="quality-profile-extend"]'));
+ expect(wrapper.find('ExtendProfileForm').exists()).toBe(true);
+
+ wrapper.find('ExtendProfileForm').prop<Function>('onExtend')(name);
+ expect(updateProfiles).toBeCalled();
+ await waitAndUpdate(wrapper);
+
+ expect(push).toBeCalledWith({
+ pathname: '/organizations/org/quality_profiles/show',
+ query: { language: 'js', name }
+ });
+ expect(wrapper.find('ExtendProfileForm').exists()).toBe(false);
+});
+
+function shallowRender(props: Partial<ProfileActions['props']> = {}) {
+ const router = mockRouter();
+ return shallow(
+ <ProfileActions
+ organization="org"
+ profile={PROFILE}
+ router={router}
+ updateProfiles={jest.fn()}
+ {...props}
+ />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Modal
+ contentLabel="quality_profiles.extend_x_title.name.JavaScript"
+ onRequestClose={[MockFunction]}
+>
+ <form>
+ <div
+ className="modal-head"
+ >
+ <h2>
+ quality_profiles.extend_x_title.name.JavaScript
+ </h2>
+ </div>
+ <div
+ className="modal-body"
+ >
+ <div
+ className="modal-field"
+ >
+ <label
+ htmlFor="extend-profile-name"
+ >
+ quality_profiles.copy_new_name
+ <em
+ className="mandatory"
+ >
+ *
+ </em>
+ </label>
+ <input
+ autoFocus={true}
+ id="extend-profile-name"
+ maxLength={100}
+ name="name"
+ onChange={[Function]}
+ required={true}
+ size={50}
+ type="text"
+ value=""
+ />
+ </div>
+ </div>
+ <div
+ className="modal-foot"
+ >
+ <DeferredSpinner
+ className="spacer-right"
+ loading={false}
+ timeout={100}
+ />
+ <SubmitButton
+ disabled={true}
+ id="extend-profile-submit"
+ onClick={[Function]}
+ >
+ copy
+ </SubmitButton>
+ <ResetButtonLink
+ id="extend-profile-cancel"
+ onClick={[MockFunction]}
+ >
+ cancel
+ </ResetButtonLink>
+ </div>
+ </form>
+</Modal>
+`;
>
copy
</ActionsDropdownItem>
+ <ActionsDropdownItem
+ id="quality-profile-extend"
+ onClick={[Function]}
+ >
+ extend
+ </ActionsDropdownItem>
<ActionsDropdownItem
id="quality-profile-rename"
onClick={[Function]}
this.setState({ parent: option ? option.value : undefined });
};
- handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+ handleFormSubmit = async (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
this.setState({ loading: true });
data.append('organization', this.props.organization);
}
- createQualityProfile(data).then(
- ({ profile }: { profile: Profile }) => {
- if (this.state.parent) {
- // eslint-disable-next-line promise/no-nesting
- changeProfileParent(profile.key, this.state.parent).then(
- () => {
- this.props.onCreate(profile);
- },
- () => {}
- );
- } else {
- this.props.onCreate(profile);
- }
- },
- () => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
+ try {
+ const { profile } = await createQualityProfile(data);
+ if (this.state.parent) {
+ await changeProfileParent(profile.key, this.state.parent);
}
- );
+ this.props.onCreate(profile);
+ } finally {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
};
render() {
edit=Edit
events=Events
example=Example
+extend=Extend
explore=Explore
false=False
favorite=Favorite
quality_profiles.parameter_set_to=Parameter {0} set to {1}
quality_profiles.x_rules_only_in={0} rules only in
quality_profiles.x_rules_have_different_configuration={0} rules have a different configuration
-quality_profiles.copy_x_title=Copy Profile {0} - {1}
+quality_profiles.copy_x_title=Copy Profile "{0}" - {1}
+quality_profiles.extend_x_title=Extend Profile "{0}" - {1}
quality_profiles.copy_new_name=New name
quality_profiles.rename_x_title=Rename Profile {0} - {1}
quality_profiles.deprecated=deprecated