瀏覽代碼

Refactor Quality Profiles page

- Move all API interactions to the parent component
- Merge copy, extend, and rename forms into a single component
- Simplify delete form
tags/8.8.0.42792
Wouter Admiraal 3 年之前
父節點
當前提交
926932fc65
共有 18 個文件被更改,包括 1158 次插入835 次删除
  1. 0
    123
      server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx
  2. 43
    72
      server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx
  3. 0
    137
      server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx
  4. 158
    93
      server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
  5. 86
    0
      server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx
  6. 0
    123
      server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx
  7. 18
    18
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx
  8. 0
    64
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx
  9. 250
    50
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx
  10. 70
    0
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx
  11. 93
    7
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap
  12. 0
    69
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap
  13. 237
    73
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
  14. 190
    0
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap
  15. 2
    1
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx
  16. 4
    4
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritanceBox-test.tsx.snap
  17. 7
    0
      server/sonar-web/src/main/js/apps/quality-profiles/types.ts
  18. 0
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 0
- 123
server/sonar-web/src/main/js/apps/quality-profiles/components/CopyProfileForm.tsx 查看文件

@@ -1,123 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { copyProfile } from '../../../api/quality-profiles';
import { Profile } from '../types';

interface Props {
onClose: () => void;
onCopy: (name: string) => void;
profile: Profile;
}

interface State {
loading: boolean;
name: string | null;
}

export default class CopyProfileForm extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false, name: null };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ name: event.currentTarget.value });
};

handleFormSubmit = (event: React.SyntheticEvent<HTMLElement>) => {
event.preventDefault();

const { name } = this.state;

if (name != null) {
this.setState({ loading: true });
copyProfile(this.props.profile.key, name).then(
(profile: any) => this.props.onCopy(profile.name),
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
}
};

render() {
const { profile } = this.props;
const header = translateWithParameters(
'quality_profiles.copy_x_title',
profile.name,
profile.languageName
);
const submitDisabled =
this.state.loading || !this.state.name || this.state.name === profile.name;

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<form id="copy-profile-form" onSubmit={this.handleFormSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>
<div className="modal-body">
<MandatoryFieldsExplanation className="modal-field" />
<div className="modal-field">
<label htmlFor="copy-profile-name">
{translate('quality_profiles.copy_new_name')}
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="copy-profile-name"
maxLength={100}
name="name"
onChange={this.handleNameChange}
required={true}
size={50}
type="text"
value={this.state.name != null ? this.state.name : profile.name}
/>
</div>
</div>
<div className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitDisabled} id="copy-profile-submit">
{translate('copy')}
</SubmitButton>
<ResetButtonLink id="copy-profile-cancel" onClick={this.props.onClose}>
{translate('cancel')}
</ResetButtonLink>
</div>
</form>
</Modal>
);
}
}

+ 43
- 72
server/sonar-web/src/main/js/apps/quality-profiles/components/DeleteProfileForm.tsx 查看文件

@@ -22,90 +22,61 @@ import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/contro
import Modal from 'sonar-ui-common/components/controls/Modal';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { deleteProfile } from '../../../api/quality-profiles';
import { Profile } from '../types';

interface Props {
export interface DeleteProfileFormProps {
loading: boolean;
onClose: () => void;
onDelete: () => void;
profile: Profile;
}

interface State {
loading: boolean;
}

export default class DeleteProfileForm extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
this.setState({ loading: true });
deleteProfile(this.props.profile).then(this.props.onDelete, () => {
if (this.mounted) {
this.setState({ loading: false });
}
});
};

render() {
const { profile } = this.props;
const header = translate('quality_profiles.delete_confirm_title');
export default function DeleteProfileForm(props: DeleteProfileFormProps) {
const { loading, profile } = props;
const header = translate('quality_profiles.delete_confirm_title');

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<form id="delete-profile-form" onSubmit={this.handleFormSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>
<div className="modal-body">
<div className="js-modal-messages" />
{profile.childrenCount > 0 ? (
<div>
<Alert variant="warning">
{translate('quality_profiles.this_profile_has_descendants')}
</Alert>
<p>
{translateWithParameters(
'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants',
profile.name,
profile.languageName
)}
</p>
</div>
) : (
return (
<Modal contentLabel={header} onRequestClose={props.onClose}>
<form
onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
props.onDelete();
}}>
<div className="modal-head">
<h2>{header}</h2>
</div>
<div className="modal-body">
{profile.childrenCount > 0 ? (
<div>
<Alert variant="warning">
{translate('quality_profiles.this_profile_has_descendants')}
</Alert>
<p>
{translateWithParameters(
'quality_profiles.are_you_sure_want_delete_profile_x',
'quality_profiles.are_you_sure_want_delete_profile_x_and_descendants',
profile.name,
profile.languageName
)}
</p>
)}
</div>
<div className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
<SubmitButton
className="button-red"
disabled={this.state.loading}
id="delete-profile-submit">
{translate('delete')}
</SubmitButton>
<ResetButtonLink id="delete-profile-cancel" onClick={this.props.onClose}>
{translate('cancel')}
</ResetButtonLink>
</div>
</form>
</Modal>
);
}
</div>
) : (
<p>
{translateWithParameters(
'quality_profiles.are_you_sure_want_delete_profile_x',
profile.name,
profile.languageName
)}
</p>
)}
</div>
<div className="modal-foot">
{loading && <i className="spinner spacer-right" />}
<SubmitButton className="button-red" disabled={loading}>
{translate('delete')}
</SubmitButton>
<ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
</div>
</form>
</Modal>
);
}

+ 0
- 137
server/sonar-web/src/main/js/apps/quality-profiles/components/ExtendProfileForm.tsx 查看文件

@@ -1,137 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { changeProfileParent, createQualityProfile } from '../../../api/quality-profiles';
import { Profile } from '../types';

interface Props {
onClose: () => void;
onExtend: (name: string) => void;
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 (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
if (this.canSubmit(this.state)) {
const { profile: parentProfile } = this.props;
const { name } = this.state;

const data = new FormData();

data.append('language', parentProfile.language);
data.append('name', name);

this.setState({ loading: true });

try {
const { profile: newProfile } = await createQualityProfile(data);
await changeProfileParent(newProfile, parentProfile);
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} size="small">
<form onSubmit={this.handleFormSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>
<div className="modal-body">
<MandatoryFieldsExplanation className="modal-field" />
<div className="modal-field">
<label htmlFor="extend-profile-name">
{translate('quality_profiles.copy_new_name')}
<MandatoryFieldMarker />
</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">
{translate('extend')}
</SubmitButton>
<ResetButtonLink id="extend-profile-cancel" onClick={this.props.onClose}>
{translate('cancel')}
</ResetButtonLink>
</div>
</form>
</Modal>
);
}
}

+ 158
- 93
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx 查看文件

@@ -23,16 +23,22 @@ import ActionsDropdown, {
ActionsDropdownItem
} from 'sonar-ui-common/components/controls/ActionsDropdown';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getQualityProfileBackupUrl, setDefaultProfile } from '../../../api/quality-profiles';
import {
changeProfileParent,
copyProfile,
createQualityProfile,
deleteProfile,
getQualityProfileBackupUrl,
renameProfile,
setDefaultProfile
} from '../../../api/quality-profiles';
import { Router, withRouter } from '../../../components/hoc/withRouter';
import { getBaseUrl } from '../../../helpers/system';
import { getRulesUrl } from '../../../helpers/urls';
import { Profile } from '../types';
import { Profile, ProfileActionModals } from '../types';
import { getProfileComparePath, getProfilePath, PROFILE_PATH } from '../utils';
import CopyProfileForm from './CopyProfileForm';
import DeleteProfileForm from './DeleteProfileForm';
import ExtendProfileForm from './ExtendProfileForm';
import RenameProfileForm from './RenameProfileForm';
import ProfileModalForm from './ProfileModalForm';

interface Props {
className?: string;
@@ -43,94 +49,129 @@ interface Props {
}

interface State {
copyFormOpen: boolean;
extendFormOpen: boolean;
deleteFormOpen: boolean;
renameFormOpen: boolean;
loading: boolean;
openModal?: ProfileActionModals;
}

export class ProfileActions extends React.PureComponent<Props, State> {
mounted = false;
state: State = {
copyFormOpen: false,
extendFormOpen: false,
deleteFormOpen: false,
renameFormOpen: false
loading: false
};

closeCopyForm = () => {
this.setState({ copyFormOpen: false });
};
componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

closeDeleteForm = () => {
this.setState({ deleteFormOpen: false });
handleCloseModal = () => {
this.setState({ openModal: undefined });
};

closeExtendForm = () => {
this.setState({ extendFormOpen: false });
handleCopyClick = () => {
this.setState({ openModal: ProfileActionModals.Copy });
};

closeRenameForm = () => {
this.setState({ renameFormOpen: false });
handleExtendClick = () => {
this.setState({ openModal: ProfileActionModals.Extend });
};

handleCopyClick = () => {
this.setState({ copyFormOpen: true });
handleRenameClick = () => {
this.setState({ openModal: ProfileActionModals.Rename });
};

handleDeleteClick = () => {
this.setState({ deleteFormOpen: true });
this.setState({ openModal: ProfileActionModals.Delete });
};

handleExtendClick = () => {
this.setState({ extendFormOpen: true });
};
handleProfileCopy = async (name: string) => {
this.setState({ loading: true });

handleRenameClick = () => {
this.setState({ renameFormOpen: true });
try {
await copyProfile(this.props.profile.key, name);
this.profileActionPerformed(name);
} catch {
this.profileActionError();
}
};

handleProfileCopy = (name: string) => {
this.closeCopyForm();
this.navigateToNewProfile(name);
};
handleProfileExtend = async (name: string) => {
const { profile: parentProfile } = this.props;

const data = {
language: parentProfile.language,
name
};

this.setState({ loading: true });

handleProfileDelete = () => {
this.props.router.replace(PROFILE_PATH);
this.props.updateProfiles();
try {
const { profile: newProfile } = await createQualityProfile(data);
await changeProfileParent(newProfile, parentProfile);
this.profileActionPerformed(name);
} catch {
this.profileActionError();
}
};

handleProfileExtend = (name: string) => {
this.closeExtendForm();
this.navigateToNewProfile(name);
handleProfileRename = async (name: string) => {
this.setState({ loading: true });

try {
await renameProfile(this.props.profile.key, name);
this.profileActionPerformed(name);
} catch {
this.profileActionError();
}
};

handleProfileRename = (name: string) => {
this.closeRenameForm();
this.props.updateProfiles().then(
() => {
if (!this.props.fromList) {
this.props.router.replace(getProfilePath(name, this.props.profile.language));
}
},
() => {}
);
handleProfileDelete = async () => {
this.setState({ loading: true });

try {
await deleteProfile(this.props.profile);

if (this.mounted) {
this.setState({ loading: false, openModal: undefined });
this.props.router.replace(PROFILE_PATH);
this.props.updateProfiles();
}
} catch {
this.profileActionError();
}
};

handleSetDefaultClick = () => {
setDefaultProfile(this.props.profile).then(this.props.updateProfiles, () => {});
};

navigateToNewProfile = (name: string) => {
this.props.updateProfiles().then(
() => {
this.props.router.push(getProfilePath(name, this.props.profile.language));
},
() => {}
);
profileActionPerformed = (name: string) => {
const { profile, router } = this.props;
if (this.mounted) {
this.setState({ loading: false, openModal: undefined });
this.props.updateProfiles().then(
() => {
router.push(getProfilePath(name, profile.language));
},
() => {
/* noop */
}
);
}
};

profileActionError = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

render() {
const { profile } = this.props;
const { loading, openModal } = this.state;
const { actions = {} } = profile;

const backupUrl = `${getBaseUrl()}${getQualityProfileBackupUrl(profile)}`;
@@ -144,86 +185,110 @@ export class ProfileActions extends React.PureComponent<Props, State> {
<>
<ActionsDropdown className={this.props.className}>
{actions.edit && (
<ActionsDropdownItem to={activateMoreUrl}>
<span data-test="quality-profiles__activate-more-rules">
{translate('quality_profiles.activate_more_rules')}
</span>
<ActionsDropdownItem
className="it__quality-profiles__activate-more-rules"
to={activateMoreUrl}>
{translate('quality_profiles.activate_more_rules')}
</ActionsDropdownItem>
)}

{!profile.isBuiltIn && (
<ActionsDropdownItem download={`${profile.key}.xml`} to={backupUrl}>
<span data-test="quality-profiles__backup">{translate('backup_verb')}</span>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download={`${profile.key}.xml`}
to={backupUrl}>
{translate('backup_verb')}
</ActionsDropdownItem>
)}

<ActionsDropdownItem to={getProfileComparePath(profile.name, profile.language)}>
<span data-test="quality-profiles__compare">{translate('compare')}</span>
<ActionsDropdownItem
className="it__quality-profiles__compare"
to={getProfileComparePath(profile.name, profile.language)}>
{translate('compare')}
</ActionsDropdownItem>

{actions.copy && (
<>
<ActionsDropdownItem onClick={this.handleCopyClick}>
<span data-test="quality-profiles__copy">{translate('copy')}</span>
<ActionsDropdownItem
className="it__quality-profiles__copy"
onClick={this.handleCopyClick}>
{translate('copy')}
</ActionsDropdownItem>

<ActionsDropdownItem onClick={this.handleExtendClick}>
<span data-test="quality-profiles__extend">{translate('extend')}</span>
<ActionsDropdownItem
className="it__quality-profiles__extend"
onClick={this.handleExtendClick}>
{translate('extend')}
</ActionsDropdownItem>
</>
)}

{actions.edit && (
<ActionsDropdownItem onClick={this.handleRenameClick}>
<span data-test="quality-profiles__rename">{translate('rename')}</span>
<ActionsDropdownItem
className="it__quality-profiles__rename"
onClick={this.handleRenameClick}>
{translate('rename')}
</ActionsDropdownItem>
)}

{actions.setAsDefault && (
<ActionsDropdownItem onClick={this.handleSetDefaultClick}>
<span data-test="quality-profiles__set-as-default">
{translate('set_as_default')}
</span>
<ActionsDropdownItem
className="it__quality-profiles__set-as-default"
onClick={this.handleSetDefaultClick}>
{translate('set_as_default')}
</ActionsDropdownItem>
)}

{actions.delete && <ActionsDropdownDivider />}

{actions.delete && (
<ActionsDropdownItem destructive={true} onClick={this.handleDeleteClick}>
<span data-test="quality-profiles__delete">{translate('delete')}</span>
<ActionsDropdownItem
className="it__quality-profiles__delete"
destructive={true}
onClick={this.handleDeleteClick}>
{translate('delete')}
</ActionsDropdownItem>
)}
</ActionsDropdown>

{this.state.copyFormOpen && (
<CopyProfileForm
onClose={this.closeCopyForm}
onCopy={this.handleProfileCopy}
{openModal === ProfileActionModals.Copy && (
<ProfileModalForm
btnLabelKey="copy"
headerKey="quality_profiles.copy_x_title"
loading={loading}
onClose={this.handleCloseModal}
onSubmit={this.handleProfileCopy}
profile={profile}
/>
)}

{this.state.extendFormOpen && (
<ExtendProfileForm
onClose={this.closeExtendForm}
onExtend={this.handleProfileExtend}
{openModal === ProfileActionModals.Extend && (
<ProfileModalForm
btnLabelKey="extend"
headerKey="quality_profiles.extend_x_title"
loading={loading}
onClose={this.handleCloseModal}
onSubmit={this.handleProfileExtend}
profile={profile}
/>
)}

{this.state.deleteFormOpen && (
<DeleteProfileForm
onClose={this.closeDeleteForm}
onDelete={this.handleProfileDelete}
{openModal === ProfileActionModals.Rename && (
<ProfileModalForm
btnLabelKey="rename"
headerKey="quality_profiles.rename_x_title"
loading={loading}
onClose={this.handleCloseModal}
onSubmit={this.handleProfileRename}
profile={profile}
/>
)}

{this.state.renameFormOpen && (
<RenameProfileForm
onClose={this.closeRenameForm}
onRename={this.handleProfileRename}
{openModal === ProfileActionModals.Delete && (
<DeleteProfileForm
loading={loading}
onClose={this.handleCloseModal}
onDelete={this.handleProfileDelete}
profile={profile}
/>
)}

+ 86
- 0
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx 查看文件

@@ -0,0 +1,86 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { Profile } from '../types';

export interface ProfileModalFormProps {
btnLabelKey: string;
headerKey: string;
loading: boolean;
onClose: () => void;
onSubmit: (name: string) => void;
profile: Profile;
}

export default function ProfileModalForm(props: ProfileModalFormProps) {
const { btnLabelKey, headerKey, loading, profile } = props;
const [name, setName] = React.useState<string | undefined>(undefined);

const submitDisabled = loading || !name || name === profile.name;
const header = translateWithParameters(headerKey, profile.name, profile.languageName);

return (
<Modal contentLabel={header} onRequestClose={props.onClose} size="small">
<form
onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
if (name) {
props.onSubmit(name);
}
}}>
<div className="modal-head">
<h2>{header}</h2>
</div>
<div className="modal-body">
<MandatoryFieldsExplanation className="modal-field" />
<div className="modal-field">
<label htmlFor="profile-name">
{translate('quality_profiles.new_name')}
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="profile-name"
maxLength={100}
name="name"
onChange={(e: React.SyntheticEvent<HTMLInputElement>) => {
setName(e.currentTarget.value);
}}
required={true}
size={50}
type="text"
value={name ?? profile.name}
/>
</div>
</div>
<div className="modal-foot">
{loading && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitDisabled}>{translate(btnLabelKey)}</SubmitButton>
<ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
</div>
</form>
</Modal>
);
}

+ 0
- 123
server/sonar-web/src/main/js/apps/quality-profiles/components/RenameProfileForm.tsx 查看文件

@@ -1,123 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { renameProfile } from '../../../api/quality-profiles';
import { Profile } from '../types';

interface Props {
onClose: () => void;
onRename: (name: string) => void;
profile: Profile;
}

interface State {
loading: boolean;
name: string | null;
}

export default class RenameProfileForm extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false, name: null };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

handleNameChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ name: event.currentTarget.value });
};

handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();

const { name } = this.state;

if (name != null) {
this.setState({ loading: true });
renameProfile(this.props.profile.key, name).then(
() => this.props.onRename(name),
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
}
};

render() {
const { profile } = this.props;
const header = translateWithParameters(
'quality_profiles.rename_x_title',
profile.name,
profile.languageName
);
const submitDisabled =
this.state.loading || !this.state.name || this.state.name === profile.name;

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<form id="rename-profile-form" onSubmit={this.handleFormSubmit}>
<div className="modal-head">
<h2>{header}</h2>
</div>
<div className="modal-body">
<MandatoryFieldsExplanation className="modal-field" />
<div className="modal-field">
<label htmlFor="rename-profile-name">
{translate('quality_profiles.new_name')}
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="rename-profile-name"
maxLength={100}
name="name"
onChange={this.handleNameChange}
required={true}
size={50}
type="text"
value={this.state.name != null ? this.state.name : profile.name}
/>
</div>
</div>
<div className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitDisabled} id="rename-profile-submit">
{translate('rename')}
</SubmitButton>
<ResetButtonLink id="rename-profile-cancel" onClick={this.props.onClose}>
{translate('cancel')}
</ResetButtonLink>
</div>
</form>
</Modal>
);
}
}

+ 18
- 18
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/DeleteProfileForm-test.tsx 查看文件

@@ -17,35 +17,35 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { deleteProfile } from '../../../../api/quality-profiles';
import { mockEvent, mockQualityProfile } from '../../../../helpers/testMocks';
import DeleteProfileForm from '../DeleteProfileForm';

beforeEach(() => jest.clearAllMocks());

jest.mock('../../../../api/quality-profiles', () => ({
deleteProfile: jest.fn().mockResolvedValue({})
}));
import DeleteProfileForm, { DeleteProfileFormProps } from '../DeleteProfileForm';

it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ loading: true })).toMatchSnapshot('loading');
expect(shallowRender({ profile: mockQualityProfile({ childrenCount: 2 }) })).toMatchSnapshot(
'profile has children'
);
});

it('should handle form submit correctly', async () => {
const wrapper = shallowRender();
wrapper.instance().handleFormSubmit(mockEvent());
await waitAndUpdate(wrapper);
it('should correctly submit the form', () => {
const onDelete = jest.fn();
const wrapper = shallowRender({ onDelete });

expect(deleteProfile).toHaveBeenCalled();
const formOnSubmit = wrapper.find('form').props().onSubmit;
if (formOnSubmit) {
formOnSubmit(mockEvent());
}
expect(onDelete).toBeCalled();
});

function shallowRender(props: Partial<DeleteProfileForm['props']> = {}) {
return shallow<DeleteProfileForm>(
function shallowRender(props: Partial<DeleteProfileFormProps> = {}) {
return shallow<DeleteProfileFormProps>(
<DeleteProfileForm
loading={false}
onClose={jest.fn()}
onDelete={jest.fn()}
profile={mockQualityProfile()}

+ 0
- 64
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ExtendProfileForm-test.tsx 查看文件

@@ -1,64 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
import * as React from 'react';
import { mockEvent, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { changeProfileParent, createQualityProfile } from '../../../../api/quality-profiles';
import { mockQualityProfile } from '../../../../helpers/testMocks';
import ExtendProfileForm from '../ExtendProfileForm';

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('should correctly create a new profile and extend the existing one', async () => {
const profile = mockQualityProfile();
const name = 'New name';
const wrapper = shallowRender({ profile });

expect(wrapper.find('SubmitButton').props().disabled).toBe(true);

wrapper.setState({ name }).update();
wrapper.instance().handleFormSubmit(mockEvent());
await waitAndUpdate(wrapper);

const data = new FormData();
data.append('language', profile.language);
data.append('name', name);
expect(createQualityProfile).toHaveBeenCalledWith(data);
expect(changeProfileParent).toHaveBeenCalledWith({ key: 'new-profile' }, profile);
});

function shallowRender(props: Partial<ExtendProfileForm['props']> = {}) {
return shallow<ExtendProfileForm>(
<ExtendProfileForm
onClose={jest.fn()}
onExtend={jest.fn()}
profile={mockQualityProfile()}
{...props}
/>
);
}

+ 250
- 50
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileActions-test.tsx 查看文件

@@ -20,16 +20,36 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { setDefaultProfile } from '../../../../api/quality-profiles';
import {
changeProfileParent,
copyProfile,
createQualityProfile,
deleteProfile,
renameProfile,
setDefaultProfile
} from '../../../../api/quality-profiles';
import { mockQualityProfile, mockRouter } from '../../../../helpers/testMocks';
import { ProfileActionModals } from '../../types';
import { PROFILE_PATH } from '../../utils';
import DeleteProfileForm from '../DeleteProfileForm';
import { ProfileActions } from '../ProfileActions';
import ProfileModalForm from '../ProfileModalForm';

beforeEach(() => jest.clearAllMocks());
jest.mock('../../../../api/quality-profiles', () => {
const { mockQualityProfile } = jest.requireActual('../../../../helpers/testMocks');

jest.mock('../../../../api/quality-profiles', () => ({
...jest.requireActual('../../../../api/quality-profiles'),
setDefaultProfile: jest.fn().mockResolvedValue({})
}));
return {
...jest.requireActual('../../../../api/quality-profiles'),
copyProfile: jest.fn().mockResolvedValue(null),
changeProfileParent: jest.fn().mockResolvedValue(null),
createQualityProfile: jest
.fn()
.mockResolvedValue({ profile: mockQualityProfile({ key: 'newProfile' }) }),
deleteProfile: jest.fn().mockResolvedValue(null),
setDefaultProfile: jest.fn().mockResolvedValue(null),
renameProfile: jest.fn().mockResolvedValue(null)
};
});

const PROFILE = mockQualityProfile({
activeRuleCount: 68,
@@ -39,15 +59,13 @@ const PROFILE = mockQualityProfile({
rulesUpdatedAt: '2017-06-28T12:58:44+0000'
});

it('renders with no permissions', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('renders with permission to edit only', () => {
expect(shallowRender({ profile: { ...PROFILE, actions: { edit: true } } })).toMatchSnapshot();
});
beforeEach(() => jest.clearAllMocks());

it('renders with all permissions', () => {
it('renders correctly', () => {
expect(shallowRender()).toMatchSnapshot('no permissions');
expect(shallowRender({ profile: { ...PROFILE, actions: { edit: true } } })).toMatchSnapshot(
'edit only'
);
expect(
shallowRender({
profile: {
@@ -61,58 +79,240 @@ it('renders with all permissions', () => {
}
}
})
).toMatchSnapshot();
).toMatchSnapshot('all permissions');

expect(shallowRender().setState({ openModal: ProfileActionModals.Copy })).toMatchSnapshot(
'copy modal'
);
expect(shallowRender().setState({ openModal: ProfileActionModals.Extend })).toMatchSnapshot(
'extend modal'
);
expect(shallowRender().setState({ openModal: ProfileActionModals.Rename })).toMatchSnapshot(
'rename modal'
);
expect(shallowRender().setState({ openModal: ProfileActionModals.Delete })).toMatchSnapshot(
'delete modal'
);
});

it('should copy 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
describe('copy a profile', () => {
it('should correctly copy a profile', async () => {
const name = 'new-name';
const updateProfiles = jest.fn().mockResolvedValue(null);
const push = jest.fn();
const wrapper = shallowRender({
profile: { ...PROFILE, actions: { copy: true } },
router: mockRouter({ push }),
updateProfiles
});

click(wrapper.find('.it__quality-profiles__copy'));
expect(wrapper.find(ProfileModalForm).exists()).toBe(true);

wrapper
.find(ProfileModalForm)
.props()
.onSubmit(name);
expect(copyProfile).toBeCalledWith(PROFILE.key, name);
await waitAndUpdate(wrapper);

expect(updateProfiles).toBeCalled();
expect(push).toBeCalledWith({
pathname: '/profiles/show',
query: { language: 'js', name }
});
expect(wrapper.find(ProfileModalForm).exists()).toBe(false);
});

click(wrapper.find('[data-test="quality-profiles__copy"]').parent());
expect(wrapper.find('CopyProfileForm').exists()).toBe(true);
it('should correctly keep the modal open in case of an error', async () => {
(copyProfile as jest.Mock).mockRejectedValueOnce(null);

wrapper.find('CopyProfileForm').prop<Function>('onCopy')(name);
expect(updateProfiles).toBeCalled();
await waitAndUpdate(wrapper);
const name = 'new-name';
const updateProfiles = jest.fn();
const push = jest.fn();
const wrapper = shallowRender({
profile: { ...PROFILE, actions: { copy: true } },
router: mockRouter({ push }),
updateProfiles
});
wrapper.setState({ openModal: ProfileActionModals.Copy });

wrapper.instance().handleProfileCopy(name);
await waitAndUpdate(wrapper);

expect(updateProfiles).not.toBeCalled();
await waitAndUpdate(wrapper);

expect(push).toBeCalledWith({
pathname: '/profiles/show',
query: { language: 'js', name }
expect(push).not.toBeCalled();
expect(wrapper.state().openModal).toBe(ProfileActionModals.Copy);
});
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
describe('extend a profile', () => {
it('should correctly extend a profile', async () => {
const name = 'new-name';
const profile = { ...PROFILE, actions: { copy: true } };
const updateProfiles = jest.fn().mockResolvedValue(null);
const push = jest.fn();
const wrapper = shallowRender({
profile,
router: mockRouter({ push }),
updateProfiles
});

click(wrapper.find('.it__quality-profiles__extend'));
expect(wrapper.find(ProfileModalForm).exists()).toBe(true);

wrapper
.find(ProfileModalForm)
.props()
.onSubmit(name);
expect(createQualityProfile).toBeCalledWith({ language: profile.language, name });
await waitAndUpdate(wrapper);
expect(changeProfileParent).toBeCalledWith(
expect.objectContaining({
key: 'newProfile'
}),
profile
);
await waitAndUpdate(wrapper);

expect(updateProfiles).toBeCalled();
await waitAndUpdate(wrapper);

expect(push).toBeCalledWith({
pathname: '/profiles/show',
query: { language: 'js', name }
});
expect(wrapper.find(ProfileModalForm).exists()).toBe(false);
});

click(wrapper.find('[data-test="quality-profiles__extend"]').parent());
expect(wrapper.find('ExtendProfileForm').exists()).toBe(true);
it('should correctly keep the modal open in case of an error', async () => {
(createQualityProfile as jest.Mock).mockRejectedValueOnce(null);

wrapper.find('ExtendProfileForm').prop<Function>('onExtend')(name);
expect(updateProfiles).toBeCalled();
await waitAndUpdate(wrapper);
const name = 'new-name';
const updateProfiles = jest.fn();
const push = jest.fn();
const wrapper = shallowRender({
profile: { ...PROFILE, actions: { copy: true } },
router: mockRouter({ push }),
updateProfiles
});
wrapper.setState({ openModal: ProfileActionModals.Extend });

wrapper.instance().handleProfileExtend(name);
await waitAndUpdate(wrapper);

expect(updateProfiles).not.toBeCalled();
expect(changeProfileParent).not.toBeCalled();
expect(push).not.toBeCalled();
expect(wrapper.state().openModal).toBe(ProfileActionModals.Extend);
});
});

describe('rename a profile', () => {
it('should correctly rename a profile', async () => {
const name = 'new-name';
const updateProfiles = jest.fn().mockResolvedValue(null);
const push = jest.fn();
const wrapper = shallowRender({
profile: { ...PROFILE, actions: { edit: true } },
router: mockRouter({ push }),
updateProfiles
});

click(wrapper.find('.it__quality-profiles__rename'));
expect(wrapper.find(ProfileModalForm).exists()).toBe(true);

wrapper
.find(ProfileModalForm)
.props()
.onSubmit(name);
expect(renameProfile).toBeCalledWith(PROFILE.key, name);
await waitAndUpdate(wrapper);

expect(updateProfiles).toBeCalled();
expect(push).toBeCalledWith({
pathname: '/profiles/show',
query: { language: 'js', name }
});
expect(wrapper.find(ProfileModalForm).exists()).toBe(false);
});

it('should correctly keep the modal open in case of an error', async () => {
(renameProfile as jest.Mock).mockRejectedValueOnce(null);

const name = 'new-name';
const updateProfiles = jest.fn();
const push = jest.fn();
const wrapper = shallowRender({
profile: { ...PROFILE, actions: { copy: true } },
router: mockRouter({ push }),
updateProfiles
});
wrapper.setState({ openModal: ProfileActionModals.Rename });

wrapper.instance().handleProfileRename(name);
await waitAndUpdate(wrapper);

expect(updateProfiles).not.toBeCalled();
await waitAndUpdate(wrapper);

expect(push).not.toBeCalled();
expect(wrapper.state().openModal).toBe(ProfileActionModals.Rename);
});
});

describe('delete a profile', () => {
it('should correctly delete a profile', async () => {
const updateProfiles = jest.fn().mockResolvedValue(null);
const replace = jest.fn();
const profile = { ...PROFILE, actions: { delete: true } };
const wrapper = shallowRender({
profile,
router: mockRouter({ replace }),
updateProfiles
});

click(wrapper.find('.it__quality-profiles__delete'));
expect(wrapper.find(DeleteProfileForm).exists()).toBe(true);

wrapper
.find(DeleteProfileForm)
.props()
.onDelete();
expect(deleteProfile).toBeCalledWith(profile);
await waitAndUpdate(wrapper);

expect(updateProfiles).toBeCalled();
expect(replace).toBeCalledWith(PROFILE_PATH);
expect(wrapper.find(ProfileModalForm).exists()).toBe(false);
});

it('should correctly keep the modal open in case of an error', async () => {
(deleteProfile as jest.Mock).mockRejectedValueOnce(null);

const updateProfiles = jest.fn();
const replace = jest.fn();
const wrapper = shallowRender({
profile: { ...PROFILE, actions: { copy: true } },
router: mockRouter({ replace }),
updateProfiles
});
wrapper.setState({ openModal: ProfileActionModals.Delete });

wrapper.instance().handleProfileDelete();
await waitAndUpdate(wrapper);

expect(updateProfiles).not.toBeCalled();
await waitAndUpdate(wrapper);

expect(push).toBeCalledWith({
pathname: '/profiles/show',
query: { language: 'js', name }
expect(replace).not.toBeCalled();
expect(wrapper.state().openModal).toBe(ProfileActionModals.Delete);
});
expect(wrapper.find('ExtendProfileForm').exists()).toBe(false);
});

it('should delete profile properly', async () => {
it('should correctly set a profile as the default', async () => {
const updateProfiles = jest.fn();

const wrapper = shallowRender({ updateProfiles });

+ 70
- 0
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx 查看文件

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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 { shallow } from 'enzyme';
import * as React from 'react';
import { change } from 'sonar-ui-common/helpers/testUtils';
import { mockEvent, mockQualityProfile } from '../../../../helpers/testMocks';
import ProfileModalForm, { ProfileModalFormProps } from '../ProfileModalForm';

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
expect(shallowRender({ loading: true })).toMatchSnapshot('loading');

const wrapper = shallowRender();
change(wrapper.find('#profile-name'), 'new name');
expect(wrapper).toMatchSnapshot('can submit');
});

it('should correctly submit the form', () => {
const onSubmit = jest.fn();
const wrapper = shallowRender({ onSubmit });

// Won't submit unless a new name was given.
let formOnSubmit = wrapper.find('form').props().onSubmit;
if (formOnSubmit) {
formOnSubmit(mockEvent());
}
expect(onSubmit).not.toBeCalled();

// Input a new name.
change(wrapper.find('#profile-name'), 'new name');

// Now will submit the form.
formOnSubmit = wrapper.find('form').props().onSubmit;
if (formOnSubmit) {
formOnSubmit(mockEvent());
}
expect(onSubmit).toBeCalledWith('new name');
});

function shallowRender(props: Partial<ProfileModalFormProps> = {}) {
return shallow<ProfileModalFormProps>(
<ProfileModalForm
btnLabelKey="btn-label"
headerKey="header-label"
loading={false}
onClose={jest.fn()}
onSubmit={jest.fn()}
profile={mockQualityProfile()}
{...props}
/>
);
}

+ 93
- 7
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/DeleteProfileForm-test.tsx.snap 查看文件

@@ -1,12 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
exports[`should render correctly: default 1`] = `
<Modal
contentLabel="quality_profiles.delete_confirm_title"
onRequestClose={[MockFunction]}
>
<form
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
quality_profiles.delete_confirm_title
</h2>
</div>
<div
className="modal-body"
>
<p>
quality_profiles.are_you_sure_want_delete_profile_x.name.JavaScript
</p>
</div>
<div
className="modal-foot"
>
<SubmitButton
className="button-red"
disabled={false}
>
delete
</SubmitButton>
<ResetButtonLink
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

exports[`should render correctly: loading 1`] = `
<Modal
contentLabel="quality_profiles.delete_confirm_title"
onRequestClose={[MockFunction]}
>
<form
id="delete-profile-form"
onSubmit={[Function]}
>
<div
@@ -19,25 +59,71 @@ exports[`should render correctly 1`] = `
<div
className="modal-body"
>
<div
className="js-modal-messages"
/>
<p>
quality_profiles.are_you_sure_want_delete_profile_x.name.JavaScript
</p>
</div>
<div
className="modal-foot"
>
<i
className="spinner spacer-right"
/>
<SubmitButton
className="button-red"
disabled={true}
>
delete
</SubmitButton>
<ResetButtonLink
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

exports[`should render correctly: profile has children 1`] = `
<Modal
contentLabel="quality_profiles.delete_confirm_title"
onRequestClose={[MockFunction]}
>
<form
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
quality_profiles.delete_confirm_title
</h2>
</div>
<div
className="modal-body"
>
<div>
<Alert
variant="warning"
>
quality_profiles.this_profile_has_descendants
</Alert>
<p>
quality_profiles.are_you_sure_want_delete_profile_x_and_descendants.name.JavaScript
</p>
</div>
</div>
<div
className="modal-foot"
>
<SubmitButton
className="button-red"
disabled={false}
id="delete-profile-submit"
>
delete
</SubmitButton>
<ResetButtonLink
id="delete-profile-cancel"
onClick={[MockFunction]}
>
cancel

+ 0
- 69
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ExtendProfileForm-test.tsx.snap 查看文件

@@ -1,69 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Modal
contentLabel="quality_profiles.extend_x_title.name.JavaScript"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
quality_profiles.extend_x_title.name.JavaScript
</h2>
</div>
<div
className="modal-body"
>
<MandatoryFieldsExplanation
className="modal-field"
/>
<div
className="modal-field"
>
<label
htmlFor="extend-profile-name"
>
quality_profiles.copy_new_name
<MandatoryFieldMarker />
</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}
/>
<SubmitButton
disabled={true}
id="extend-profile-submit"
>
extend
</SubmitButton>
<ResetButtonLink
id="extend-profile-cancel"
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

+ 237
- 73
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap 查看文件

@@ -1,9 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders with all permissions 1`] = `
exports[`renders correctly: all permissions 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdownItem
className="it__quality-profiles__activate-more-rules"
to={
Object {
"pathname": "/coding_rules",
@@ -14,23 +15,17 @@ exports[`renders with all permissions 1`] = `
}
}
>
<span
data-test="quality-profiles__activate-more-rules"
>
quality_profiles.activate_more_rules
</span>
quality_profiles.activate_more_rules
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
>
<span
data-test="quality-profiles__backup"
>
backup_verb
</span>
backup_verb
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__compare"
to={
Object {
"pathname": "/profiles/compare",
@@ -41,77 +36,56 @@ exports[`renders with all permissions 1`] = `
}
}
>
<span
data-test="quality-profiles__compare"
>
compare
</span>
compare
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__copy"
onClick={[Function]}
>
<span
data-test="quality-profiles__copy"
>
copy
</span>
copy
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__extend"
onClick={[Function]}
>
<span
data-test="quality-profiles__extend"
>
extend
</span>
extend
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__rename"
onClick={[Function]}
>
<span
data-test="quality-profiles__rename"
>
rename
</span>
rename
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__set-as-default"
onClick={[Function]}
>
<span
data-test="quality-profiles__set-as-default"
>
set_as_default
</span>
set_as_default
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
className="it__quality-profiles__delete"
destructive={true}
onClick={[Function]}
>
<span
data-test="quality-profiles__delete"
>
delete
</span>
delete
</ActionsDropdownItem>
</ActionsDropdown>
</Fragment>
`;

exports[`renders with no permissions 1`] = `
exports[`renders correctly: copy modal 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
>
<span
data-test="quality-profiles__backup"
>
backup_verb
</span>
backup_verb
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__compare"
to={
Object {
"pathname": "/profiles/compare",
@@ -122,20 +96,91 @@ exports[`renders with no permissions 1`] = `
}
}
>
<span
data-test="quality-profiles__compare"
>
compare
</span>
compare
</ActionsDropdownItem>
</ActionsDropdown>
<ProfileModalForm
btnLabelKey="copy"
headerKey="quality_profiles.copy_x_title"
loading={false}
onClose={[Function]}
onSubmit={[Function]}
profile={
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 68,
"childrenCount": 0,
"depth": 0,
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
"key": "key",
"language": "js",
"languageName": "JavaScript",
"name": "name",
"projectCount": 3,
"rulesUpdatedAt": "2017-06-28T12:58:44+0000",
}
}
/>
</Fragment>
`;

exports[`renders correctly: delete modal 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
>
backup_verb
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__compare"
to={
Object {
"pathname": "/profiles/compare",
"query": Object {
"language": "js",
"name": "name",
},
}
}
>
compare
</ActionsDropdownItem>
</ActionsDropdown>
<DeleteProfileForm
loading={false}
onClose={[Function]}
onDelete={[Function]}
profile={
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 68,
"childrenCount": 0,
"depth": 0,
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
"key": "key",
"language": "js",
"languageName": "JavaScript",
"name": "name",
"projectCount": 3,
"rulesUpdatedAt": "2017-06-28T12:58:44+0000",
}
}
/>
</Fragment>
`;

exports[`renders with permission to edit only 1`] = `
exports[`renders correctly: edit only 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdownItem
className="it__quality-profiles__activate-more-rules"
to={
Object {
"pathname": "/coding_rules",
@@ -146,23 +191,17 @@ exports[`renders with permission to edit only 1`] = `
}
}
>
<span
data-test="quality-profiles__activate-more-rules"
>
quality_profiles.activate_more_rules
</span>
quality_profiles.activate_more_rules
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
>
<span
data-test="quality-profiles__backup"
>
backup_verb
</span>
backup_verb
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__compare"
to={
Object {
"pathname": "/profiles/compare",
@@ -173,21 +212,146 @@ exports[`renders with permission to edit only 1`] = `
}
}
>
<span
data-test="quality-profiles__compare"
>
compare
</span>
compare
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__rename"
onClick={[Function]}
>
<span
data-test="quality-profiles__rename"
>
rename
</span>
rename
</ActionsDropdownItem>
</ActionsDropdown>
</Fragment>
`;

exports[`renders correctly: extend modal 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
>
backup_verb
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__compare"
to={
Object {
"pathname": "/profiles/compare",
"query": Object {
"language": "js",
"name": "name",
},
}
}
>
compare
</ActionsDropdownItem>
</ActionsDropdown>
<ProfileModalForm
btnLabelKey="extend"
headerKey="quality_profiles.extend_x_title"
loading={false}
onClose={[Function]}
onSubmit={[Function]}
profile={
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 68,
"childrenCount": 0,
"depth": 0,
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
"key": "key",
"language": "js",
"languageName": "JavaScript",
"name": "name",
"projectCount": 3,
"rulesUpdatedAt": "2017-06-28T12:58:44+0000",
}
}
/>
</Fragment>
`;

exports[`renders correctly: no permissions 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
>
backup_verb
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__compare"
to={
Object {
"pathname": "/profiles/compare",
"query": Object {
"language": "js",
"name": "name",
},
}
}
>
compare
</ActionsDropdownItem>
</ActionsDropdown>
</Fragment>
`;

exports[`renders correctly: rename modal 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
to="/api/qualityprofiles/backup?language=js&qualityProfile=name"
>
backup_verb
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__compare"
to={
Object {
"pathname": "/profiles/compare",
"query": Object {
"language": "js",
"name": "name",
},
}
}
>
compare
</ActionsDropdownItem>
</ActionsDropdown>
<ProfileModalForm
btnLabelKey="rename"
headerKey="quality_profiles.rename_x_title"
loading={false}
onClose={[Function]}
onSubmit={[Function]}
profile={
Object {
"activeDeprecatedRuleCount": 0,
"activeRuleCount": 68,
"childrenCount": 0,
"depth": 0,
"isBuiltIn": false,
"isDefault": false,
"isInherited": false,
"key": "key",
"language": "js",
"languageName": "JavaScript",
"name": "name",
"projectCount": 3,
"rulesUpdatedAt": "2017-06-28T12:58:44+0000",
}
}
/>
</Fragment>
`;

+ 190
- 0
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap 查看文件

@@ -0,0 +1,190 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly: can submit 1`] = `
<Modal
contentLabel="header-label.name.JavaScript"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
header-label.name.JavaScript
</h2>
</div>
<div
className="modal-body"
>
<MandatoryFieldsExplanation
className="modal-field"
/>
<div
className="modal-field"
>
<label
htmlFor="profile-name"
>
quality_profiles.new_name
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="profile-name"
maxLength={100}
name="name"
onChange={[Function]}
required={true}
size={50}
type="text"
value="new name"
/>
</div>
</div>
<div
className="modal-foot"
>
<SubmitButton
disabled={false}
>
btn-label
</SubmitButton>
<ResetButtonLink
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

exports[`should render correctly: default 1`] = `
<Modal
contentLabel="header-label.name.JavaScript"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
header-label.name.JavaScript
</h2>
</div>
<div
className="modal-body"
>
<MandatoryFieldsExplanation
className="modal-field"
/>
<div
className="modal-field"
>
<label
htmlFor="profile-name"
>
quality_profiles.new_name
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="profile-name"
maxLength={100}
name="name"
onChange={[Function]}
required={true}
size={50}
type="text"
value="name"
/>
</div>
</div>
<div
className="modal-foot"
>
<SubmitButton
disabled={true}
>
btn-label
</SubmitButton>
<ResetButtonLink
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

exports[`should render correctly: loading 1`] = `
<Modal
contentLabel="header-label.name.JavaScript"
onRequestClose={[MockFunction]}
size="small"
>
<form
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
header-label.name.JavaScript
</h2>
</div>
<div
className="modal-body"
>
<MandatoryFieldsExplanation
className="modal-field"
/>
<div
className="modal-field"
>
<label
htmlFor="profile-name"
>
quality_profiles.new_name
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="profile-name"
maxLength={100}
name="name"
onChange={[Function]}
required={true}
size={50}
type="text"
value="name"
/>
</div>
</div>
<div
className="modal-foot"
>
<i
className="spinner spacer-right"
/>
<SubmitButton
disabled={true}
>
btn-label
</SubmitButton>
<ResetButtonLink
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

+ 2
- 1
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileInheritanceBox.tsx 查看文件

@@ -17,6 +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 * as classNames from 'classnames';
import * as React from 'react';
import HelpTooltip from 'sonar-ui-common/components/controls/HelpTooltip';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
@@ -46,7 +47,7 @@ export default function ProfileInheritanceBox(props: Props) {
const offset = 25 * depth;

return (
<tr className={className} data-test={`quality-profiles__inheritance-${type}`}>
<tr className={classNames(`it__quality-profiles__inheritance-${type}`, className)}>
<td>
<div style={{ paddingLeft: offset }}>
{displayLink ? (

+ 4
- 4
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileInheritanceBox-test.tsx.snap 查看文件

@@ -2,7 +2,7 @@

exports[`should render correctly 1`] = `
<tr
data-test="quality-profiles__inheritance-current"
className="it__quality-profiles__inheritance-current"
>
<td>
<div
@@ -34,7 +34,7 @@ exports[`should render correctly 1`] = `

exports[`should render correctly 2`] = `
<tr
data-test="quality-profiles__inheritance-current"
className="it__quality-profiles__inheritance-current"
>
<td>
<div
@@ -69,7 +69,7 @@ exports[`should render correctly 2`] = `

exports[`should render correctly 3`] = `
<tr
data-test="quality-profiles__inheritance-current"
className="it__quality-profiles__inheritance-current"
>
<td>
<div
@@ -105,7 +105,7 @@ exports[`should render correctly 3`] = `

exports[`should render correctly 4`] = `
<tr
data-test="quality-profiles__inheritance-current"
className="it__quality-profiles__inheritance-current"
>
<td>
<div

+ 7
- 0
server/sonar-web/src/main/js/apps/quality-profiles/types.ts 查看文件

@@ -38,3 +38,10 @@ export interface ProfileChangelogEvent {
ruleKey: string;
ruleName: string;
}

export enum ProfileActionModals {
Copy,
Extend,
Rename,
Delete
}

+ 0
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -1506,7 +1506,6 @@ 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.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
quality_profiles.severity_set_to=Severity set to

Loading…
取消
儲存