Переглянути джерело

SONAR-12338 Better document the Extend Quality Profile action

tags/9.8.0.63668
Mathieu Suen 1 рік тому
джерело
коміт
361c8e51ef
26 змінених файлів з 933 додано та 883 видалено
  1. 225
    0
      server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts
  2. 2
    2
      server/sonar-web/src/main/js/api/quality-profiles.ts
  3. 1
    0
      server/sonar-web/src/main/js/app/styles/init/forms.css
  4. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
  5. 3
    3
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
  6. 177
    0
      server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx
  7. 25
    14
      server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx
  8. 24
    6
      server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx
  9. 0
    69
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx
  10. 35
    18
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap
  11. 0
    190
      server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap
  12. 29
    1
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx
  13. 198
    98
      server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx
  14. 0
    90
      server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/CreateProfileForm-test.tsx
  15. 0
    320
      server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/CreateProfileForm-test.tsx.snap
  16. 27
    0
      server/sonar-web/src/main/js/apps/quality-profiles/styles.css
  17. 4
    4
      server/sonar-web/src/main/js/apps/quality-profiles/types.ts
  18. 45
    31
      server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx
  19. 3
    1
      server/sonar-web/src/main/js/components/controls/RadioCard.tsx
  20. 3
    1
      server/sonar-web/src/main/js/components/controls/__tests__/ActionsDropdown-test.tsx
  21. 3
    3
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap
  22. 42
    0
      server/sonar-web/src/main/js/components/icons/CopyQualityProfileIcon.tsx
  23. 42
    0
      server/sonar-web/src/main/js/components/icons/ExtendQualityProfileIcon.tsx
  24. 12
    17
      server/sonar-web/src/main/js/components/icons/NewQualityProfileIcon.tsx
  25. 1
    1
      server/sonar-web/src/main/js/helpers/l10n.ts
  26. 31
    13
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 225
- 0
server/sonar-web/src/main/js/api/mocks/QualityProfilesServiceMock.ts Переглянути файл

@@ -0,0 +1,225 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { cloneDeep } from 'lodash';
import { RequestData } from '../../helpers/request';
import { mockQualityProfile } from '../../helpers/testMocks';
import { SearchRulesResponse } from '../../types/coding-rules';
import { Dict, Paging, ProfileInheritanceDetails } from '../../types/types';
import {
changeProfileParent,
copyProfile,
createQualityProfile,
getImporters,
getProfileInheritance,
getProfileProjects,
Profile,
ProfileProject,
searchQualityProfiles,
SearchQualityProfilesParameters,
SearchQualityProfilesResponse,
} from '../quality-profiles';
import { searchRules } from '../rules';

export default class QualityProfilesServiceMock {
isAdmin = false;
listQualityProfile: Profile[] = [];

languageMapping: Dict<Partial<Profile>> = {
c: { language: 'c', languageName: 'C' },
};

constructor() {
this.resetQualityProfile();

(searchQualityProfiles as jest.Mock).mockImplementation(this.handleSearchQualityProfiles);
(createQualityProfile as jest.Mock).mockImplementation(this.handleCreateQualityProfile);
(changeProfileParent as jest.Mock).mockImplementation(this.handleChangeProfileParent);
(getProfileInheritance as jest.Mock).mockImplementation(this.handleGetProfileInheritance);
(getProfileProjects as jest.Mock).mockImplementation(this.handleGetProfileProjects);
(copyProfile as jest.Mock).mockImplementation(this.handleCopyProfile);
(getImporters as jest.Mock).mockImplementation(this.handleGetImporters);
(searchRules as jest.Mock).mockImplementation(this.handleSearchRules);
}

resetQualityProfile() {
this.listQualityProfile = [
mockQualityProfile({
key: 'c-qp',
language: 'c',
languageName: 'C',
name: 'c quality profile',
activeDeprecatedRuleCount: 0,
}),
mockQualityProfile({
key: 'java-qp',
language: 'java',
name: 'java quality profile',
activeDeprecatedRuleCount: 0,
}),
];
}

handleGetImporters = () => {
return this.reply([]);
};

handleCopyProfile = (fromKey: string, name: string): Promise<Profile> => {
const profile = this.listQualityProfile.find((p) => p.key === fromKey);
if (!profile) {
return Promise.reject({
errors: [{ msg: `No profile has been found for ${fromKey}` }],
});
}

const copiedQualityProfile = mockQualityProfile({
...profile,
name,
key: `qp${this.listQualityProfile.length}`,
});

this.listQualityProfile.push(copiedQualityProfile);

return this.reply(copiedQualityProfile);
};

handleGetProfileProjects = (): Promise<{
more: boolean;
paging: Paging;
results: ProfileProject[];
}> => {
return this.reply({
more: false,
paging: {
pageIndex: 0,
pageSize: 10,
total: 0,
},
results: [],
});
};

handleGetProfileInheritance = ({
language,
name,
}: Profile): Promise<{
ancestors: ProfileInheritanceDetails[];
children: ProfileInheritanceDetails[];
profile: ProfileInheritanceDetails;
}> => {
const profile = this.listQualityProfile.find((p) => p.name === name && p.language === language);
if (!profile) {
return Promise.reject({
errors: [{ msg: `No profile has been found for ${language} ${name}` }],
});
}

// Lets fake it for now
return this.reply({
ancestors: [],
children: [],
profile: {
name: profile.name,
activeRuleCount: 0,
isBuiltIn: false,
key: profile.key,
},
});
};

handleChangeProfileParent = ({ language, name }: Profile, parentProfile?: Profile) => {
const profile = this.listQualityProfile.find((p) => p.name === name && p.language === language);

if (!profile) {
return Promise.reject({
errors: [{ msg: `No profile has been found for ${language} ${name}` }],
});
}

profile.parentKey = parentProfile?.key;
profile.parentName = parentProfile?.name;

return Promise.resolve({});
};

handleCreateQualityProfile = (data: RequestData | FormData) => {
if (data instanceof FormData) {
const name = data.get('name') as string;
const language = data.get('language') as string;
const newQualityProfile = mockQualityProfile({
name,
...this.languageMapping[language],
key: `qp${this.listQualityProfile.length}`,
});

this.listQualityProfile.push(newQualityProfile);

return this.reply({ profile: newQualityProfile });
}
const newQualityProfile = mockQualityProfile({
name: data.name,
...this.languageMapping[data.language],
key: `qp${this.listQualityProfile.length}`,
});

this.listQualityProfile.push(newQualityProfile);

return this.reply({ profile: newQualityProfile });
};

handleSearchRules = (): Promise<SearchRulesResponse> => {
return this.reply({
p: 0,
ps: 500,
total: 0,
rules: [],
});
};

handleSearchQualityProfiles = (
parameters: SearchQualityProfilesParameters = {}
): Promise<SearchQualityProfilesResponse> => {
const { language } = parameters;
let profiles = this.listQualityProfile;
if (language) {
profiles = profiles.filter((p) => p.language === language);
}
if (this.isAdmin) {
profiles = profiles.map((p) => ({ ...p, actions: { copy: true } }));
}

return this.reply({
actions: { create: this.isAdmin },
profiles,
});
};

setAdmin() {
this.isAdmin = true;
}

reset() {
this.isAdmin = false;
this.resetQualityProfile();
}

reply<T>(response: T): Promise<T> {
return Promise.resolve(cloneDeep(response));
}
}

+ 2
- 2
server/sonar-web/src/main/js/api/quality-profiles.ts Переглянути файл

@@ -125,8 +125,8 @@ export function renameProfile(key: string, name: string) {
return post('/api/qualityprofiles/rename', { key, name }).catch(throwGlobalError);
}

export function copyProfile(fromKey: string, toName: string): Promise<any> {
return postJSON('/api/qualityprofiles/copy', { fromKey, toName }).catch(throwGlobalError);
export function copyProfile(fromKey: string, name: string): Promise<Profile> {
return postJSON('/api/qualityprofiles/copy', { fromKey, toName: name }).catch(throwGlobalError);
}

export function deleteProfile({ language, name: qualityProfile }: Profile) {

+ 1
- 0
server/sonar-web/src/main/js/app/styles/init/forms.css Переглянути файл

@@ -213,6 +213,7 @@ em.mandatory {
.form-field input[type='password'],
.form-field textarea,
.form-field select,
.form-field .react-select,
.form-field .Select {
width: 250px;
}

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx Переглянути файл

@@ -125,7 +125,7 @@ export function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
<ClickEventBoundary>
<div className="project-activity-analysis-actions big-spacer-left">
<ActionsDropdown
ariaLabel={translateWithParameters(
label={translateWithParameters(
'project_activity.analysis_X_actions',
analysis.buildString || formatDate(parsedDate, formatterOption)
)}

+ 3
- 3
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx Переглянути файл

@@ -82,9 +82,9 @@ const ui = {
cogBtn: (id: string) => byRole('button', { name: `project_activity.analysis_X_actions.${id}` }),
seeDetailsBtn: (time: string) =>
byRole('button', { name: `project_activity.show_analysis_X_on_graph.${time}` }),
addCustomEventBtn: byRole('link', { name: 'project_activity.add_custom_event' }),
addVersionEvenBtn: byRole('link', { name: 'project_activity.add_version' }),
deleteAnalysisBtn: byRole('link', { name: 'project_activity.delete_analysis' }),
addCustomEventBtn: byRole('button', { name: 'project_activity.add_custom_event' }),
addVersionEvenBtn: byRole('button', { name: 'project_activity.add_version' }),
deleteAnalysisBtn: byRole('button', { name: 'project_activity.delete_analysis' }),
editEventBtn: byRole('button', { name: 'project_activity.events.tooltip.edit' }),
deleteEventBtn: byRole('button', { name: 'project_activity.events.tooltip.delete' }),


+ 177
- 0
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfilesApp-it.tsx Переглянути файл

@@ -0,0 +1,177 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { getByText } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import selectEvent from 'react-select-event';
import { byRole } from 'testing-library-selector';
import QualityProfilesServiceMock from '../../../api/mocks/QualityProfilesServiceMock';
import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
import routes from '../routes';

jest.mock('../../../api/quality-profiles');
jest.mock('../../../api/rules');

const serviceMock = new QualityProfilesServiceMock();

beforeEach(() => {
serviceMock.reset();
});

const ui = {
cQualityProfileName: 'c quality profile',
newCQualityProfileName: 'New c quality profile',
newCQualityProfileNameFromCreateButton: 'New c quality profile from create',

actionOnCQualityProfile: byRole('button', {
name: 'quality_profiles.actions.c quality profile.C',
}),
extendButton: byRole('button', {
name: 'extend',
}),
copyButton: byRole('button', {
name: 'copy',
}),
createButton: byRole('button', { name: 'create' }),
popup: byRole('dialog'),
copyRadio: byRole('radio', {
name: 'quality_profiles.creation_from_copy quality_profiles.creation_from_copy_description_1 quality_profiles.creation_from_copy_description_2',
}),
blankRadio: byRole('radio', {
name: 'quality_profiles.creation_from_blank quality_profiles.creation_from_blank_description',
}),
namePropupInput: byRole('textbox', { name: 'quality_profiles.new_name field_required' }),
filterByLang: byRole('textbox', { name: 'quality_profiles.filter_by:' }),
listLinkCQualityProfile: byRole('link', { name: 'c quality profile' }),
listLinkNewCQualityProfile: byRole('link', { name: 'New c quality profile' }),
listLinkNewCQualityProfileFromCreateButton: byRole('link', {
name: 'New c quality profile from create',
}),
listLinkJavaQualityProfile: byRole('link', { name: 'java quality profile' }),
returnToList: byRole('link', { name: 'quality_profiles.page' }),
languageSelect: byRole('textbox', { name: 'language field_required' }),
profileExtendSelect: byRole('textbox', {
name: 'quality_profiles.creation.choose_parent_quality_profile field_required',
}),
profileCopySelect: byRole('textbox', {
name: 'quality_profiles.creation.choose_copy_quality_profile field_required',
}),
nameCreatePopupInput: byRole('textbox', { name: 'name field_required' }),
};

it('should list Quality Profiles and filter by language', async () => {
const user = userEvent.setup();
serviceMock.setAdmin();
renderQualityProfiles();
expect(await ui.listLinkCQualityProfile.find()).toBeInTheDocument();
expect(ui.listLinkJavaQualityProfile.get()).toBeInTheDocument();

await selectEvent.select(ui.filterByLang.get(), 'C');

expect(ui.listLinkJavaQualityProfile.query()).not.toBeInTheDocument();

// Creation form should have language pre-selected
await user.click(await ui.createButton.find());
// eslint-disable-next-line testing-library/prefer-screen-queries
expect(getByText(ui.popup.get(), 'C')).toBeInTheDocument();
await selectEvent.select(ui.profileExtendSelect.get(), ui.cQualityProfileName);
await user.type(ui.nameCreatePopupInput.get(), ui.newCQualityProfileName);

expect(ui.createButton.get(ui.popup.get())).toBeEnabled();
});

it('should be able to extend Quality Profile', async () => {
// From the list page
const user = userEvent.setup();
serviceMock.setAdmin();
renderQualityProfiles();

await user.click(await ui.actionOnCQualityProfile.find());
await user.click(ui.extendButton.get());

await user.clear(ui.namePropupInput.get());
await user.type(ui.namePropupInput.get(), ui.newCQualityProfileName);
await user.click(ui.extendButton.get());
expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument();

// From the create form
await user.click(ui.returnToList.get());
await user.click(ui.createButton.get());

await selectEvent.select(ui.languageSelect.get(), 'C');
await selectEvent.select(ui.profileExtendSelect.get(), ui.newCQualityProfileName);
await user.type(ui.nameCreatePopupInput.get(), ui.newCQualityProfileNameFromCreateButton);
await user.click(ui.createButton.get(ui.popup.get()));

expect(await ui.listLinkNewCQualityProfileFromCreateButton.find()).toBeInTheDocument();
});

it('should be able to copy Quality Profile', async () => {
// From the list page
const user = userEvent.setup();
serviceMock.setAdmin();
renderQualityProfiles();

await user.click(await ui.actionOnCQualityProfile.find());
await user.click(ui.copyButton.get());

await user.clear(ui.namePropupInput.get());
await user.type(ui.namePropupInput.get(), ui.newCQualityProfileName);
await user.click(ui.copyButton.get(ui.popup.get()));
expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument();

// From the create form
await user.click(ui.returnToList.get());
await user.click(ui.createButton.get());

await user.click(ui.copyRadio.get());
await selectEvent.select(ui.languageSelect.get(), 'C');
await selectEvent.select(ui.profileCopySelect.get(), ui.newCQualityProfileName);
await user.type(ui.nameCreatePopupInput.get(), ui.newCQualityProfileNameFromCreateButton);
await user.click(ui.createButton.get(ui.popup.get()));

expect(await ui.listLinkNewCQualityProfileFromCreateButton.find()).toBeInTheDocument();
});

it('should be able to create blank Quality Profile', async () => {
// From the list page
const user = userEvent.setup();
serviceMock.setAdmin();
renderQualityProfiles();

await user.click(await ui.createButton.find());

await user.click(ui.blankRadio.get());
await selectEvent.select(ui.languageSelect.get(), 'C');
await user.type(ui.nameCreatePopupInput.get(), ui.newCQualityProfileName);
await user.click(ui.createButton.get(ui.popup.get()));

expect(await ui.listLinkNewCQualityProfile.find()).toBeInTheDocument();
});

function renderQualityProfiles() {
renderAppRoutes('profiles', routes, {
languages: {
js: { key: 'js', name: 'JavaScript' },
java: { key: 'java', name: 'Java' },
c: { key: 'c', name: 'C' },
},
});
}

+ 25
- 14
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileActions.tsx Переглянути файл

@@ -33,7 +33,7 @@ import ActionsDropdown, {
} from '../../../components/controls/ActionsDropdown';
import Tooltip from '../../../components/controls/Tooltip';
import { Router, withRouter } from '../../../components/hoc/withRouter';
import { translate } from '../../../helpers/l10n';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getBaseUrl } from '../../../helpers/system';
import { getRulesUrl } from '../../../helpers/urls';
import { Profile, ProfileActionModals } from '../types';
@@ -190,7 +190,14 @@ export class ProfileActions extends React.PureComponent<Props, State> {

return (
<>
<ActionsDropdown className={this.props.className}>
<ActionsDropdown
className={this.props.className}
label={translateWithParameters(
'quality_profiles.actions',
profile.name,
profile.languageName
)}
>
{actions.edit && (
<ActionsDropdownItem
className="it__quality-profiles__activate-more-rules"
@@ -220,17 +227,24 @@ export class ProfileActions extends React.PureComponent<Props, State> {
{actions.copy && (
<>
<ActionsDropdownItem
className="it__quality-profiles__copy"
onClick={this.handleCopyClick}
tooltipPlacement="left"
tooltipOverlay={translateWithParameters(
'quality_profiles.extend_help',
profile.name
)}
className="it__quality-profiles__extend"
onClick={this.handleExtendClick}
>
{translate('copy')}
{translate('extend')}
</ActionsDropdownItem>

<ActionsDropdownItem
className="it__quality-profiles__extend"
onClick={this.handleExtendClick}
tooltipPlacement="left"
tooltipOverlay={translateWithParameters('quality_profiles.copy_help', profile.name)}
className="it__quality-profiles__copy"
onClick={this.handleCopyClick}
>
{translate('extend')}
{translate('copy')}
</ActionsDropdownItem>
</>
)}
@@ -280,8 +294,7 @@ export class ProfileActions extends React.PureComponent<Props, State> {

{openModal === ProfileActionModals.Copy && (
<ProfileModalForm
btnLabelKey="copy"
headerKey="quality_profiles.copy_x_title"
action={openModal}
loading={loading}
onClose={this.handleCloseModal}
onSubmit={this.handleProfileCopy}
@@ -291,8 +304,7 @@ export class ProfileActions extends React.PureComponent<Props, State> {

{openModal === ProfileActionModals.Extend && (
<ProfileModalForm
btnLabelKey="extend"
headerKey="quality_profiles.extend_x_title"
action={openModal}
loading={loading}
onClose={this.handleCloseModal}
onSubmit={this.handleProfileExtend}
@@ -302,8 +314,7 @@ export class ProfileActions extends React.PureComponent<Props, State> {

{openModal === ProfileActionModals.Rename && (
<ProfileModalForm
btnLabelKey="rename"
headerKey="quality_profiles.rename_x_title"
action={openModal}
loading={loading}
onClose={this.handleCloseModal}
onSubmit={this.handleProfileRename}

+ 24
- 6
server/sonar-web/src/main/js/apps/quality-profiles/components/ProfileModalForm.tsx Переглянути файл

@@ -23,23 +23,30 @@ import Modal from '../../../components/controls/Modal';
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Profile } from '../types';
import { Dict } from '../../../types/types';
import { Profile, ProfileActionModals } from '../types';

export interface ProfileModalFormProps {
btnLabelKey: string;
headerKey: string;
action: ProfileActionModals.Copy | ProfileActionModals.Extend | ProfileActionModals.Rename;
loading: boolean;
onClose: () => void;
onSubmit: (name: string) => void;
profile: Profile;
}

const LABELS_FOR_ACTION: Dict<{ button: string; header: string }> = {
[ProfileActionModals.Copy]: { button: 'copy', header: 'quality_profiles.copy_x_title' },
[ProfileActionModals.Rename]: { button: 'rename', header: 'quality_profiles.rename_x_title' },
[ProfileActionModals.Extend]: { button: 'extend', header: 'quality_profiles.extend_x_title' },
};

export default function ProfileModalForm(props: ProfileModalFormProps) {
const { btnLabelKey, headerKey, loading, profile } = props;
const { action, 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);
const labels = LABELS_FOR_ACTION[action];
const header = translateWithParameters(labels.header, profile.name, profile.languageName);

return (
<Modal contentLabel={header} onRequestClose={props.onClose} size="small">
@@ -55,6 +62,17 @@ export default function ProfileModalForm(props: ProfileModalFormProps) {
<h2>{header}</h2>
</div>
<div className="modal-body">
{action === ProfileActionModals.Copy && (
<p className="spacer-bottom">
{translateWithParameters('quality_profiles.copy_help', profile.name)}
</p>
)}
{action === ProfileActionModals.Extend && (
<p className="spacer-bottom">
{translateWithParameters('quality_profiles.extend_help', profile.name)}
</p>
)}

<MandatoryFieldsExplanation className="modal-field" />
<div className="modal-field">
<label htmlFor="profile-name">
@@ -78,7 +96,7 @@ export default function ProfileModalForm(props: ProfileModalFormProps) {
</div>
<div className="modal-foot">
{loading && <i className="spinner spacer-right" />}
<SubmitButton disabled={submitDisabled}>{translate(btnLabelKey)}</SubmitButton>
<SubmitButton disabled={submitDisabled}>{translate(labels.button)}</SubmitButton>
<ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
</div>
</form>

+ 0
- 69
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileModalForm-test.tsx Переглянути файл

@@ -1,69 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { mockQualityProfile } from '../../../../helpers/testMocks';
import { change, mockEvent } from '../../../../helpers/testUtils';
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.toHaveBeenCalled();

// 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).toHaveBeenCalledWith('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}
/>
);
}

+ 35
- 18
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileActions-test.tsx.snap Переглянути файл

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

exports[`renders correctly: all permissions 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdown
label="quality_profiles.actions.name.JavaScript"
>
<ActionsDropdownItem
className="it__quality-profiles__activate-more-rules"
to={
@@ -33,16 +35,20 @@ exports[`renders correctly: all permissions 1`] = `
compare
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__copy"
className="it__quality-profiles__extend"
onClick={[Function]}
tooltipOverlay="quality_profiles.extend_help.name"
tooltipPlacement="left"
>
copy
extend
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__extend"
className="it__quality-profiles__copy"
onClick={[Function]}
tooltipOverlay="quality_profiles.copy_help.name"
tooltipPlacement="left"
>
extend
copy
</ActionsDropdownItem>
<ActionsDropdownItem
className="it__quality-profiles__rename"
@@ -70,7 +76,9 @@ exports[`renders correctly: all permissions 1`] = `

exports[`renders correctly: copy modal 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdown
label="quality_profiles.actions.name.JavaScript"
>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
@@ -91,8 +99,7 @@ exports[`renders correctly: copy modal 1`] = `
</ActionsDropdownItem>
</ActionsDropdown>
<ProfileModalForm
btnLabelKey="copy"
headerKey="quality_profiles.copy_x_title"
action="COPY"
loading={false}
onClose={[Function]}
onSubmit={[Function]}
@@ -119,7 +126,9 @@ exports[`renders correctly: copy modal 1`] = `

exports[`renders correctly: delete modal 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdown
label="quality_profiles.actions.name.JavaScript"
>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
@@ -166,7 +175,9 @@ exports[`renders correctly: delete modal 1`] = `

exports[`renders correctly: edit only 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdown
label="quality_profiles.actions.name.JavaScript"
>
<ActionsDropdownItem
className="it__quality-profiles__activate-more-rules"
to={
@@ -208,7 +219,9 @@ exports[`renders correctly: edit only 1`] = `

exports[`renders correctly: extend modal 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdown
label="quality_profiles.actions.name.JavaScript"
>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
@@ -229,8 +242,7 @@ exports[`renders correctly: extend modal 1`] = `
</ActionsDropdownItem>
</ActionsDropdown>
<ProfileModalForm
btnLabelKey="extend"
headerKey="quality_profiles.extend_x_title"
action="EXTEND"
loading={false}
onClose={[Function]}
onSubmit={[Function]}
@@ -257,7 +269,9 @@ exports[`renders correctly: extend modal 1`] = `

exports[`renders correctly: no permissions 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdown
label="quality_profiles.actions.name.JavaScript"
>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
@@ -282,7 +296,9 @@ exports[`renders correctly: no permissions 1`] = `

exports[`renders correctly: rename modal 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdown
label="quality_profiles.actions.name.JavaScript"
>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"
@@ -303,8 +319,7 @@ exports[`renders correctly: rename modal 1`] = `
</ActionsDropdownItem>
</ActionsDropdown>
<ProfileModalForm
btnLabelKey="rename"
headerKey="quality_profiles.rename_x_title"
action="RENAME"
loading={false}
onClose={[Function]}
onSubmit={[Function]}
@@ -331,7 +346,9 @@ exports[`renders correctly: rename modal 1`] = `

exports[`should not allow to set a profile as the default if the profile has no active rules 1`] = `
<Fragment>
<ActionsDropdown>
<ActionsDropdown
label="quality_profiles.actions.name.JavaScript"
>
<ActionsDropdownItem
className="it__quality-profiles__backup"
download="key.xml"

+ 0
- 190
server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/__snapshots__/ProfileModalForm-test.tsx.snap Переглянути файл

@@ -1,190 +0,0 @@
// 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>
`;

+ 29
- 1
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileHeader.tsx Переглянути файл

@@ -18,11 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
import Link from '../../../components/common/Link';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import Tooltip from '../../../components/controls/Tooltip';
import DateFromNow from '../../../components/intl/DateFromNow';
import { translate } from '../../../helpers/l10n';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { getQualityProfileUrl } from '../../../helpers/urls';
import BuiltInQualityProfileBadge from '../components/BuiltInQualityProfileBadge';
import ProfileActions from '../components/ProfileActions';
import ProfileLink from '../components/ProfileLink';
@@ -90,6 +93,31 @@ export default class ProfileHeader extends React.PureComponent<Props> {
{translate('quality_profiles.built_in.description')}
</div>
)}

{profile.parentKey && profile.parentName && (
<div className="page-description">
<FormattedMessage
defaultMessage={translate('quality_profiles.extend_description')}
id="quality_profiles.extend_description"
values={{
link: (
<>
<Link to={getQualityProfileUrl(profile.parentName, profile.language)}>
{profile.parentName}
</Link>
<HelpTooltip
className="little-spacer-left"
overlay={translateWithParameters(
'quality_profiles.extend_description_help',
profile.parentName
)}
/>
</>
),
}}
/>
</div>
)}
</div>
);
}

+ 198
- 98
server/sonar-web/src/main/js/apps/quality-profiles/home/CreateProfileForm.tsx Переглянути файл

@@ -21,18 +21,23 @@ import { sortBy } from 'lodash';
import * as React from 'react';
import {
changeProfileParent,
copyProfile,
createQualityProfile,
getImporters,
} from '../../../api/quality-profiles';
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import Modal from '../../../components/controls/Modal';
import RadioCard from '../../../components/controls/RadioCard';
import Select from '../../../components/controls/Select';
import ValidationInput from '../../../components/controls/ValidationInput';
import { Location } from '../../../components/hoc/withRouter';
import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
import CopyQualityProfileIcon from '../../../components/icons/CopyQualityProfileIcon';
import ExtendQualityProfileIcon from '../../../components/icons/ExtendQualityProfileIcon';
import NewQualityProfileIcon from '../../../components/icons/NewQualityProfileIcon';
import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
import { translate } from '../../../helpers/l10n';
import { parseAsOptionalString } from '../../../helpers/query';
import { Profile } from '../types';
import { Profile, ProfileActionModals } from '../types';

interface Props {
languages: Array<{ key: string; name: string }>;
@@ -44,20 +49,34 @@ interface Props {

interface State {
importers: Array<{ key: string; languages: Array<string>; name: string }>;
action?: ProfileActionModals.Copy | ProfileActionModals.Extend;
language?: string;
loading: boolean;
name: string;
parent?: string;
profile?: string;
preloading: boolean;
isValidName?: boolean;
isValidProflie?: boolean;
isValidLanguage?: boolean;
}

export default class CreateProfileForm extends React.PureComponent<Props, State> {
mounted = false;
state: State = { importers: [], loading: false, name: '', preloading: true };
state: State = {
importers: [],
loading: false,
name: '',
preloading: true,
action: ProfileActionModals.Extend,
};

componentDidMount() {
this.mounted = true;
this.fetchImporters();
const languageQueryFilter = parseAsOptionalString(this.props.location.query.language);
if (languageQueryFilter !== undefined) {
this.setState({ language: languageQueryFilter, isValidLanguage: true });
}
}

componentWillUnmount() {
@@ -79,34 +98,57 @@ export default class CreateProfileForm extends React.PureComponent<Props, State>
);
}

handleSelectExtend = () => {
this.setState({ action: ProfileActionModals.Extend });
};

handleSelectCopy = () => {
this.setState({ action: ProfileActionModals.Copy });
};

handleSelectBlank = () => {
this.setState({ action: undefined });
};

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

handleLanguageChange = (option: { value: string }) => {
this.setState({ language: option.value });
this.setState({ language: option.value, isValidLanguage: true });
};

handleParentChange = (option: { value: string } | null) => {
this.setState({ parent: option ? option.value : undefined });
handleQualityProfileChange = (option: { value: string } | null) => {
this.setState({ profile: option ? option.value : undefined, isValidProflie: option !== null });
};

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

this.setState({ loading: true });

const data = new FormData(event.currentTarget);
const { action, language, name, profile: parent } = this.state;

try {
const { profile } = await createQualityProfile(data);
if (action === ProfileActionModals.Copy && parent && name) {
const profile = await copyProfile(parent, name);
this.props.onCreate(profile);
} else if (action === ProfileActionModals.Extend) {
const { profile } = await createQualityProfile({ language, name });

const parentProfile = this.props.profiles.find((p) => p.key === this.state.parent);
if (parentProfile) {
await changeProfileParent(profile, parentProfile);
}
const parentProfile = this.props.profiles.find((p) => p.key === parent);
if (parentProfile) {
await changeProfileParent(profile, parentProfile);
}

this.props.onCreate(profile);
this.props.onCreate(profile);
} else {
const data = new FormData(event.currentTarget);
const { profile } = await createQualityProfile(data);
this.props.onCreate(profile);
}
} finally {
if (this.mounted) {
this.setState({ loading: false });
@@ -114,43 +156,43 @@ export default class CreateProfileForm extends React.PureComponent<Props, State>
}
};

canSubmit() {
const { action, isValidName, isValidProflie, isValidLanguage } = this.state;

return (
(action === undefined && isValidName && isValidLanguage) ||
(action !== undefined && isValidLanguage && isValidName && isValidProflie)
);
}

render() {
const header = translate('quality_profiles.new_profile');
const { action, isValidName, isValidProflie, isValidLanguage } = this.state;
const languageQueryFilter = parseAsOptionalString(this.props.location.query.language);
const languages = sortBy(this.props.languages, 'name');
let profiles: Array<{ label: string; value: string }> = [];

const selectedLanguage = this.state.language || languageQueryFilter || languages[0].key;
const importers = this.state.importers.filter((importer) =>
importer.languages.includes(selectedLanguage)
);
const selectedLanguage = this.state.language || languageQueryFilter;
const importers = selectedLanguage
? this.state.importers.filter((importer) => importer.languages.includes(selectedLanguage))
: [];

const languageProfiles = this.props.profiles.filter((p) => p.language === selectedLanguage);
const profiles = sortBy(languageProfiles, 'name').map((profile) => ({
label: profile.isBuiltIn
? `${profile.name} (${translate('quality_profiles.built_in')})`
: profile.name,
value: profile.key,
}));

if (selectedLanguage) {
const languageProfiles = this.props.profiles.filter((p) => p.language === selectedLanguage);
profiles = [
{ label: translate('none'), value: '' },
...sortBy(languageProfiles, 'name').map((profile) => ({
label: profile.isBuiltIn
? `${profile.name} (${translate('quality_profiles.built_in')})`
: profile.name,
value: profile.key,
})),
];
}
const languagesOptions = languages.map((l) => ({
label: l.name,
value: l.key,
}));

const isParentProfileClearable = () => {
if (this.state.parent !== undefined && this.state.parent !== '') {
return true;
}
return false;
};
const canSubmit = this.canSubmit();

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="small">
<Modal contentLabel={header} onRequestClose={this.props.onClose} size="medium">
<form id="create-profile-form" onSubmit={this.handleFormSubmit}>
<div className="modal-head">
<h2>{header}</h2>
@@ -162,33 +204,65 @@ export default class CreateProfileForm extends React.PureComponent<Props, State>
</div>
) : (
<div className="modal-body">
<MandatoryFieldsExplanation className="modal-field" />
<div className="modal-field">
<label htmlFor="create-profile-name">
{translate('name')}
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="create-profile-name"
maxLength={100}
name="name"
onChange={this.handleNameChange}
required={true}
size={50}
type="text"
value={this.state.name}
/>
</div>
<div className="modal-field">
<label htmlFor="create-profile-language-input">
{translate('language')}
<MandatoryFieldMarker />
<fieldset className="modal-field big-spacer-bottom">
<label className="spacer-top">
{translate('quality_profiles.chose_creation_type')}
</label>
<div className="display-flex-row spacer-top">
<RadioCard
noRadio={true}
selected={action === ProfileActionModals.Extend}
onClick={this.handleSelectExtend}
title={<ExtendQualityProfileIcon size={64} />}
>
<h3 className="spacer-bottom h4">
{translate('quality_profiles.creation_from_extend')}
</h3>
<p className="spacer-bottom">
{translate('quality_profiles.creation_from_extend_description_1')}
</p>
<p>{translate('quality_profiles.creation_from_extend_description_2')}</p>
</RadioCard>
<RadioCard
noRadio={true}
selected={action === ProfileActionModals.Copy}
onClick={this.handleSelectCopy}
title={<CopyQualityProfileIcon size={64} />}
>
<h3 className="spacer-bottom h4">
{translate('quality_profiles.creation_from_copy')}
</h3>
<p className="spacer-bottom">
{translate('quality_profiles.creation_from_copy_description_1')}
</p>
<p>{translate('quality_profiles.creation_from_copy_description_2')}</p>
</RadioCard>
<RadioCard
noRadio={true}
onClick={this.handleSelectBlank}
selected={action === undefined}
title={<NewQualityProfileIcon size={64} />}
>
<h3 className="spacer-bottom h4">
{translate('quality_profiles.creation_from_blank')}
</h3>
<p>{translate('quality_profiles.creation_from_blank_description')}</p>
</RadioCard>
</div>
</fieldset>

<MandatoryFieldsExplanation className="modal-field" />

<ValidationInput
className="form-field"
id="create-profile-language-input"
label={translate('language')}
required={true}
isInvalid={isValidLanguage !== undefined && !isValidLanguage}
isValid={!!isValidLanguage}
>
<Select
className="width-100"
autoFocus={true}
id="create-profile-language"
inputId="create-profile-language-input"
name="language"
isClearable={false}
@@ -197,54 +271,80 @@ export default class CreateProfileForm extends React.PureComponent<Props, State>
isSearchable={true}
value={languagesOptions.filter((o) => o.value === selectedLanguage)}
/>
</div>
{selectedLanguage && profiles.length > 0 && (
<div className="modal-field">
<label htmlFor="create-profile-parent-input">
{translate('quality_profiles.parent')}
</label>
</ValidationInput>
{action !== undefined && (
<ValidationInput
className="form-field"
id="create-profile-parent-input"
label={translate(
action === ProfileActionModals.Copy
? 'quality_profiles.creation.choose_copy_quality_profile'
: 'quality_profiles.creation.choose_parent_quality_profile'
)}
required={true}
isInvalid={isValidProflie !== undefined && !isValidProflie}
isValid={!!isValidProflie}
>
<Select
className="width-100"
autoFocus={true}
id="create-profile-parent"
inputId="create-profile-parent-input"
name="parentKey"
isClearable={isParentProfileClearable()}
onChange={this.handleParentChange}
isClearable={false}
onChange={this.handleQualityProfileChange}
options={profiles}
isSearchable={true}
value={profiles.filter((o) => o.value === (this.state.parent || ''))}
value={profiles.filter((o) => o.value === this.state.profile)}
/>
</div>
</ValidationInput>
)}
{importers.map((importer) => (
<div
className="modal-field spacer-bottom js-importer"
data-key={importer.key}
key={importer.key}
>
<label htmlFor={'create-profile-form-backup-' + importer.key}>
{importer.name}
</label>
<input
id={'create-profile-form-backup-' + importer.key}
name={'backup_' + importer.key}
type="file"
/>
<p className="note">
{translate('quality_profiles.optional_configuration_file')}
</p>
</div>
))}
{/* drop me when we stop supporting ie11 */}
<input name="hello-ie11" type="hidden" value="" />
<ValidationInput
className="form-field"
id="create-profile-name"
label={translate('name')}
error={translate('quality_profiles.name_invalid')}
required={true}
isInvalid={isValidName !== undefined && !isValidName}
isValid={!!isValidName}
>
<input
autoFocus={true}
id="create-profile-name"
maxLength={100}
name="name"
onChange={this.handleNameChange}
size={50}
type="text"
value={this.state.name}
/>
</ValidationInput>

{action === undefined &&
importers.map((importer) => (
<div
className="modal-field spacer-bottom js-importer"
data-key={importer.key}
key={importer.key}
>
<label htmlFor={'create-profile-form-backup-' + importer.key}>
{importer.name}
</label>
<input
id={'create-profile-form-backup-' + importer.key}
name={'backup_' + importer.key}
type="file"
/>
<p className="note">
{translate('quality_profiles.optional_configuration_file')}
</p>
</div>
))}
</div>
)}

<div className="modal-foot">
{this.state.loading && <i className="spinner spacer-right" />}
{!this.state.preloading && (
<SubmitButton disabled={this.state.loading} id="create-profile-submit">
<SubmitButton disabled={this.state.loading || !canSubmit} id="create-profile-submit">
{translate('create')}
</SubmitButton>
)}

+ 0
- 90
server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/CreateProfileForm-test.tsx Переглянути файл

@@ -1,90 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { changeProfileParent, createQualityProfile } from '../../../../api/quality-profiles';
import { mockLocation, mockQualityProfile } from '../../../../helpers/testMocks';
import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils';
import CreateProfileForm from '../CreateProfileForm';

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

jest.mock('../../../../api/quality-profiles', () => ({
changeProfileParent: jest.fn().mockResolvedValue({}),
createQualityProfile: jest.fn().mockResolvedValue({}),
getImporters: jest.fn().mockResolvedValue([
{
key: 'key_importer',
languages: ['lang1_importer', 'lang2_importer', 'js'],
name: 'name_importer',
},
]),
}));

it('should render correctly', async () => {
const wrapper = shallowRender();
await waitAndUpdate(wrapper);

expect(wrapper).toMatchSnapshot('default');
expect(
wrapper.setProps({ location: mockLocation({ query: { language: 'js' } }) })
).toMatchSnapshot('with query filter');
});

it('should handle form submit correctly', async () => {
const onCreate = jest.fn();

const wrapper = shallowRender({ onCreate });
wrapper.instance().handleParentChange({ value: 'key' });
wrapper.instance().handleFormSubmit(mockEvent({ currentTarget: undefined }));
await waitAndUpdate(wrapper);

expect(createQualityProfile).toHaveBeenCalled();
expect(changeProfileParent).toHaveBeenCalled();
expect(onCreate).toHaveBeenCalled();
});

it('should handle form submit without parent correctly', async () => {
const onCreate = jest.fn();

const wrapper = shallowRender({ onCreate });
wrapper.instance().handleFormSubmit(mockEvent({ currentTarget: undefined }));
await waitAndUpdate(wrapper);

expect(createQualityProfile).toHaveBeenCalled();
expect(changeProfileParent).not.toHaveBeenCalled();
expect(onCreate).toHaveBeenCalled();
});

function shallowRender(props?: Partial<CreateProfileForm['props']>) {
return shallow<CreateProfileForm>(
<CreateProfileForm
languages={[
{ key: 'js', name: 'JavaScript' },
{ key: 'css', name: 'CSS' },
]}
location={mockLocation()}
onClose={jest.fn()}
onCreate={jest.fn()}
profiles={[mockQualityProfile(), mockQualityProfile({ language: 'css' })]}
{...props}
/>
);
}

+ 0
- 320
server/sonar-web/src/main/js/apps/quality-profiles/home/__tests__/__snapshots__/CreateProfileForm-test.tsx.snap Переглянути файл

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

exports[`should render correctly: default 1`] = `
<Modal
contentLabel="quality_profiles.new_profile"
onRequestClose={[MockFunction]}
size="small"
>
<form
id="create-profile-form"
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
quality_profiles.new_profile
</h2>
</div>
<div
className="modal-body"
>
<MandatoryFieldsExplanation
className="modal-field"
/>
<div
className="modal-field"
>
<label
htmlFor="create-profile-name"
>
name
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="create-profile-name"
maxLength={100}
name="name"
onChange={[Function]}
required={true}
size={50}
type="text"
value=""
/>
</div>
<div
className="modal-field"
>
<label
htmlFor="create-profile-language-input"
>
language
<MandatoryFieldMarker />
</label>
<Select
autoFocus={true}
className="width-100"
id="create-profile-language"
inputId="create-profile-language-input"
isClearable={false}
isSearchable={true}
name="language"
onChange={[Function]}
options={
Array [
Object {
"label": "CSS",
"value": "css",
},
Object {
"label": "JavaScript",
"value": "js",
},
]
}
value={
Array [
Object {
"label": "CSS",
"value": "css",
},
]
}
/>
</div>
<div
className="modal-field"
>
<label
htmlFor="create-profile-parent-input"
>
quality_profiles.parent
</label>
<Select
autoFocus={true}
className="width-100"
id="create-profile-parent"
inputId="create-profile-parent-input"
isClearable={false}
isSearchable={true}
name="parentKey"
onChange={[Function]}
options={
Array [
Object {
"label": "none",
"value": "",
},
Object {
"label": "name",
"value": "key",
},
]
}
value={
Array [
Object {
"label": "none",
"value": "",
},
]
}
/>
</div>
<input
name="hello-ie11"
type="hidden"
value=""
/>
</div>
<div
className="modal-foot"
>
<SubmitButton
disabled={false}
id="create-profile-submit"
>
create
</SubmitButton>
<ResetButtonLink
id="create-profile-cancel"
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

exports[`should render correctly: with query filter 1`] = `
<Modal
contentLabel="quality_profiles.new_profile"
onRequestClose={[MockFunction]}
size="small"
>
<form
id="create-profile-form"
onSubmit={[Function]}
>
<div
className="modal-head"
>
<h2>
quality_profiles.new_profile
</h2>
</div>
<div
className="modal-body"
>
<MandatoryFieldsExplanation
className="modal-field"
/>
<div
className="modal-field"
>
<label
htmlFor="create-profile-name"
>
name
<MandatoryFieldMarker />
</label>
<input
autoFocus={true}
id="create-profile-name"
maxLength={100}
name="name"
onChange={[Function]}
required={true}
size={50}
type="text"
value=""
/>
</div>
<div
className="modal-field"
>
<label
htmlFor="create-profile-language-input"
>
language
<MandatoryFieldMarker />
</label>
<Select
autoFocus={true}
className="width-100"
id="create-profile-language"
inputId="create-profile-language-input"
isClearable={false}
isSearchable={true}
name="language"
onChange={[Function]}
options={
Array [
Object {
"label": "CSS",
"value": "css",
},
Object {
"label": "JavaScript",
"value": "js",
},
]
}
value={
Array [
Object {
"label": "JavaScript",
"value": "js",
},
]
}
/>
</div>
<div
className="modal-field"
>
<label
htmlFor="create-profile-parent-input"
>
quality_profiles.parent
</label>
<Select
autoFocus={true}
className="width-100"
id="create-profile-parent"
inputId="create-profile-parent-input"
isClearable={false}
isSearchable={true}
name="parentKey"
onChange={[Function]}
options={
Array [
Object {
"label": "none",
"value": "",
},
Object {
"label": "name",
"value": "key",
},
]
}
value={
Array [
Object {
"label": "none",
"value": "",
},
]
}
/>
</div>
<div
className="modal-field spacer-bottom js-importer"
data-key="key_importer"
key="key_importer"
>
<label
htmlFor="create-profile-form-backup-key_importer"
>
name_importer
</label>
<input
id="create-profile-form-backup-key_importer"
name="backup_key_importer"
type="file"
/>
<p
className="note"
>
quality_profiles.optional_configuration_file
</p>
</div>
<input
name="hello-ie11"
type="hidden"
value=""
/>
</div>
<div
className="modal-foot"
>
<SubmitButton
disabled={false}
id="create-profile-submit"
>
create
</SubmitButton>
<ResetButtonLink
id="create-profile-cancel"
onClick={[MockFunction]}
>
cancel
</ResetButtonLink>
</div>
</form>
</Modal>
`;

+ 27
- 0
server/sonar-web/src/main/js/apps/quality-profiles/styles.css Переглянути файл

@@ -105,3 +105,30 @@
max-width: 270px;
word-break: break-word;
}

#create-profile-form .radio-card {
width: 245px;
background-color: var(--neutral50);
border: 1px solid var(--neutral200);
}

#create-profile-form .radio-card.selected {
background-color: var(--info50);
border: 1px solid var(--info500);
}

#create-profile-form .radio-card:hover:not(.selected) {
border: 1px solid var(--info500);
}

#create-profile-form fieldset > div {
justify-content: space-between;
}

#create-profile-form .radio-card-header {
justify-content: space-around;
}

#create-profile-form .radio-card-body {
justify-content: flex-start;
}

+ 4
- 4
server/sonar-web/src/main/js/apps/quality-profiles/types.ts Переглянути файл

@@ -41,8 +41,8 @@ export interface ProfileChangelogEvent {
}

export enum ProfileActionModals {
Copy,
Extend,
Rename,
Delete,
Copy = 'COPY',
Extend = 'EXTEND',
Rename = 'RENAME',
Delete = 'DELETE',
}

+ 45
- 31
server/sonar-web/src/main/js/components/controls/ActionsDropdown.tsx Переглянути файл

@@ -24,13 +24,14 @@ import Link from '../common/Link';
import DropdownIcon from '../icons/DropdownIcon';
import SettingsIcon from '../icons/SettingsIcon';
import { PopupPlacement } from '../ui/popups';
import { Button } from './buttons';
import { Button, ButtonPlain } from './buttons';
import Dropdown from './Dropdown';
import Tooltip, { Placement } from './Tooltip';

export interface ActionsDropdownProps {
ariaLabel?: string;
className?: string;
children: React.ReactNode;
label?: string;
onOpen?: () => void;
overlayPlacement?: PopupPlacement;
small?: boolean;
@@ -38,7 +39,7 @@ export interface ActionsDropdownProps {
}

export default function ActionsDropdown(props: ActionsDropdownProps) {
const { ariaLabel, children, className, overlayPlacement, small, toggleClassName } = props;
const { children, className, label, overlayPlacement, small, toggleClassName } = props;
return (
<Dropdown
className={className}
@@ -47,7 +48,7 @@ export default function ActionsDropdown(props: ActionsDropdownProps) {
overlayPlacement={overlayPlacement}
>
<Button
aria-label={ariaLabel}
aria-label={label}
className={classNames('dropdown-toggle', toggleClassName, {
'button-small': small,
})}
@@ -63,6 +64,8 @@ interface ItemProps {
className?: string;
children: React.ReactNode;
destructive?: boolean;
tooltipOverlay?: React.ReactNode;
tooltipPlacement?: Placement;
/** used to pass a name of downloaded file */
download?: string;
id?: string;
@@ -71,9 +74,11 @@ interface ItemProps {
}

export class ActionsDropdownItem extends React.PureComponent<ItemProps> {
handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
handleClick = (event?: React.SyntheticEvent<HTMLAnchorElement>) => {
if (event) {
event.preventDefault();
event.currentTarget.blur();
}
if (this.props.onClick) {
this.props.onClick();
}
@@ -81,39 +86,48 @@ export class ActionsDropdownItem extends React.PureComponent<ItemProps> {

render() {
const className = classNames(this.props.className, { 'text-danger': this.props.destructive });
let { children } = this.props;
const { tooltipOverlay, tooltipPlacement } = this.props;

if (this.props.download && typeof this.props.to === 'string') {
return (
<li>
<a
className={className}
download={this.props.download}
href={this.props.to}
id={this.props.id}
>
{this.props.children}
</a>
</li>
children = (
<a
className={className}
download={this.props.download}
href={this.props.to}
id={this.props.id}
>
{children}
</a>
);
} else if (this.props.to) {
children = (
<Link className={className} id={this.props.id} to={this.props.to}>
{children}
</Link>
);
} else {
children = (
<ButtonPlain
className={className}
preventDefault={true}
id={this.props.id}
onClick={this.handleClick}
>
{children}
</ButtonPlain>
);
}

if (this.props.to) {
if (tooltipOverlay !== undefined) {
return (
<li>
<Link className={className} id={this.props.id} to={this.props.to}>
{this.props.children}
</Link>
</li>
<Tooltip overlay={tooltipOverlay} placement={tooltipPlacement}>
<li>{children}</li>
</Tooltip>
);
}

return (
<li>
<a className={className} href="#" id={this.props.id} onClick={this.handleClick}>
{this.props.children}
</a>
</li>
);
return <li>{children}</li>;
}
}


+ 3
- 1
server/sonar-web/src/main/js/components/controls/RadioCard.tsx Переглянути файл

@@ -30,6 +30,7 @@ export interface RadioCardProps {
disabled?: boolean;
onClick?: () => void;
selected?: boolean;
noRadio?: boolean;
}

interface Props extends RadioCardProps {
@@ -49,6 +50,7 @@ export default function RadioCard(props: Props) {
selected,
titleInfo,
vertical = false,
noRadio = false,
} = props;
const isActionable = Boolean(onClick);
return (
@@ -70,7 +72,7 @@ export default function RadioCard(props: Props) {
>
<h2 className="radio-card-header big-spacer-bottom">
<span className="display-flex-center link-radio">
{isActionable && (
{isActionable && !noRadio && (
<i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} />
)}
{props.title}

+ 3
- 1
server/sonar-web/src/main/js/components/controls/__tests__/ActionsDropdown-test.tsx Переглянути файл

@@ -26,6 +26,7 @@ import ActionsDropdown, {
ActionsDropdownItem,
ActionsDropdownProps,
} from '../ActionsDropdown';
import { ButtonPlain } from '../buttons';

describe('ActionsDropdown', () => {
it('should render correctly', () => {
@@ -59,7 +60,8 @@ describe('ActionsDropdownItem', () => {
it('should trigger click', () => {
const onClick = jest.fn();
const wrapper = shallowRender({ onClick });
click(wrapper.find('a'));

click(wrapper.find(ButtonPlain));
expect(onClick).toHaveBeenCalled();
});


+ 3
- 3
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/ActionsDropdown-test.tsx.snap Переглянути файл

@@ -64,15 +64,15 @@ exports[`ActionsDropdownDivider should render correctly 1`] = `

exports[`ActionsDropdownItem should render correctly 1`] = `
<li>
<a
<ButtonPlain
className="foo"
href="#"
onClick={[Function]}
preventDefault={true}
>
<span>
Hello world
</span>
</a>
</ButtonPlain>
</li>
`;


+ 42
- 0
server/sonar-web/src/main/js/components/icons/CopyQualityProfileIcon.tsx Переглянути файл

@@ -0,0 +1,42 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 Icon, { IconProps } from './Icon';

export default function CopyQualityProfileIcon({
...iconProps
}: Omit<IconProps, 'viewBox' | 'fill'>) {
return (
<Icon {...iconProps} viewBox="0 0 82 64">
<path
d="M25.2293 20.4493L20.0751 15.2943C19.2414 14.4656 18.1157 14 16.9485 14L4.42014 14C1.97871 14 0 15.9787 0 18.4201L0.000448921 44.941C0.000448921 47.3818 1.97915 49.3612 4.42059 49.3612H22.1007C24.5318 49.3612 26.5209 47.3721 26.5209 44.941V23.5724C26.5209 22.4052 26.0581 21.2794 25.2293 20.4493ZM23.2058 44.941C23.2058 45.5513 22.711 46.0461 22.1007 46.0461H4.42153C3.81113 46.0461 3.31649 45.5513 3.31649 44.941L3.31511 18.4291C3.31511 17.8189 3.80989 17.3241 4.42014 17.3241H15.4705V22.8403C15.4705 24.0607 16.4602 25.0504 17.6806 25.0504H23.1436V44.941H23.2058ZM6.63022 33.3381C6.63022 34.2567 7.37611 34.9957 8.28777 34.9957H13.2604H18.2331C19.1517 34.9957 19.8906 34.2567 19.8906 33.3381C19.8906 32.4196 19.1517 31.6806 18.2331 31.6806H8.28777C7.37611 31.6806 6.63022 32.4265 6.63022 33.3381ZM18.2331 38.3108H8.28777C7.37611 38.3108 6.63022 39.0567 6.63022 39.9684C6.63022 40.88 7.37266 41.6259 8.28777 41.6259H18.2331C19.1482 41.6259 19.8906 40.8835 19.8906 39.9684C19.8906 39.0532 19.1517 38.3108 18.2331 38.3108Z"
fill="#666666"
/>
<path
d="M80.2293 20.4493L75.0751 15.2943C74.2414 14.4656 73.1157 14 71.9485 14L59.4201 14C56.9787 14 55 15.9787 55 18.4201L55.0004 44.941C55.0004 47.3818 56.9792 49.3612 59.4206 49.3612H77.1007C79.5318 49.3612 81.5209 47.3721 81.5209 44.941V23.5724C81.5209 22.4052 81.0581 21.2794 80.2293 20.4493ZM78.2058 44.941C78.2058 45.5513 77.711 46.0461 77.1007 46.0461H59.4215C58.8111 46.0461 58.3165 45.5513 58.3165 44.941L58.3151 18.4291C58.3151 17.8189 58.8099 17.3241 59.4201 17.3241H70.4705V22.8403C70.4705 24.0607 71.4602 25.0504 72.6806 25.0504H78.1436V44.941H78.2058ZM61.6302 33.3381C61.6302 34.2567 62.3761 34.9957 63.2878 34.9957H68.2604H73.2331C74.1517 34.9957 74.8906 34.2567 74.8906 33.3381C74.8906 32.4196 74.1517 31.6806 73.2331 31.6806H63.2878C62.3761 31.6806 61.6302 32.4265 61.6302 33.3381ZM73.2331 38.3108H63.2878C62.3761 38.3108 61.6302 39.0567 61.6302 39.9684C61.6302 40.88 62.3727 41.6259 63.2878 41.6259H73.2331C74.1482 41.6259 74.8906 40.8835 74.8906 39.9684C74.8906 39.0532 74.1517 38.3108 73.2331 38.3108Z"
fill="#236A97"
/>
<path
d="M50.3424 33.4672C50.8937 32.916 50.8937 32.0208 50.3424 31.4695L43.2864 24.4134C42.7351 23.8622 41.8399 23.8622 41.2886 24.4134C40.7374 24.9647 40.7374 25.8599 41.2886 26.4112L45.9412 31.0594H32.4112C31.6306 31.0594 31 31.69 31 32.4706C31 33.2512 31.6306 33.8818 32.4112 33.8818H45.9368L41.293 38.53C40.7418 39.0812 40.7418 39.9765 41.293 40.5277C41.8443 41.079 42.7395 41.079 43.2908 40.5277L50.3468 33.4717L50.3424 33.4672Z"
fill="#666666"
/>
</Icon>
);
}

+ 42
- 0
server/sonar-web/src/main/js/components/icons/ExtendQualityProfileIcon.tsx Переглянути файл

@@ -0,0 +1,42 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 Icon, { IconProps } from './Icon';

export default function ExtendQualityProfileIcon({
...iconProps
}: Omit<IconProps, 'viewBox' | 'fill'>) {
return (
<Icon {...iconProps} viewBox="0 0 82 64">
<path
d="M72.576 35.0882L67.4217 29.9332C66.5881 29.1046 65.4624 28.6389 64.2952 28.6389L51.7668 28.6389C49.3254 28.6389 47.3467 30.6176 47.3467 33.0591L47.3471 59.5799C47.3471 62.0207 49.3258 64.0001 51.7673 64.0001H69.4474C71.8785 64.0001 73.8675 62.011 73.8675 59.5799V38.2113C73.8675 37.0441 73.4048 35.9183 72.576 35.0882ZM70.5524 59.5799C70.5524 60.1902 70.0577 60.685 69.4474 60.685H51.7682C51.1578 60.685 50.6632 60.1902 50.6632 59.5799L50.6618 33.068C50.6618 32.4578 51.1566 31.963 51.7668 31.963H62.8172V37.4792C62.8172 38.6996 63.8069 39.6893 65.0273 39.6893H70.4903V59.5799H70.5524ZM53.9769 47.9771C53.9769 48.8956 54.7228 49.6346 55.6344 49.6346H60.6071H65.5798C66.4983 49.6346 67.2373 48.8956 67.2373 47.9771C67.2373 47.0585 66.4983 46.3195 65.5798 46.3195H55.6344C54.7228 46.3195 53.9769 47.0654 53.9769 47.9771ZM65.5798 52.9497H55.6344C54.7228 52.9497 53.9769 53.6956 53.9769 54.6073C53.9769 55.5189 54.7193 56.2648 55.6344 56.2648H65.5798C66.4949 56.2648 67.2373 55.5224 67.2373 54.6073C67.2373 53.6922 66.4983 52.9497 65.5798 52.9497Z"
fill="#236A97"
/>
<path
d="M33.9916 6.44927L28.8373 1.29428C28.0036 0.465641 26.8779 6.90647e-06 25.7107 6.90647e-06L13.1824 0C10.7409 0 8.76221 1.97871 8.76221 4.42014L8.76266 30.941C8.76266 33.3818 10.7414 35.3612 13.1828 35.3612H30.8629C33.294 35.3612 35.2831 33.3721 35.2831 30.941V9.57238C35.2831 8.40519 34.8203 7.27943 33.9916 6.44927ZM31.968 30.941C31.968 31.5513 31.4732 32.0461 30.8629 32.0461H13.1837C12.5733 32.0461 12.0787 31.5513 12.0787 30.941L12.0773 4.42913C12.0773 3.81887 12.5721 3.32409 13.1824 3.32409H24.2327V8.8403C24.2327 10.0607 25.2224 11.0504 26.4428 11.0504H31.9058V30.941H31.968ZM15.3924 19.3381C15.3924 20.2567 16.1383 20.9957 17.05 20.9957H22.0226H26.9953C27.9139 20.9957 28.6529 20.2567 28.6529 19.3381C28.6529 18.4196 27.9139 17.6806 26.9953 17.6806H17.05C16.1383 17.6806 15.3924 18.4265 15.3924 19.3381ZM26.9953 24.3108H17.05C16.1383 24.3108 15.3924 25.0567 15.3924 25.9684C15.3924 26.88 16.1349 27.6259 17.05 27.6259H26.9953C27.9104 27.6259 28.6529 26.8835 28.6529 25.9684C28.6529 25.0532 27.9139 24.3108 26.9953 24.3108Z"
fill="#666666"
/>
<path
d="M23.3694 40.4087C23.3694 39.6032 22.7187 38.9525 21.9132 38.9525C21.1078 38.9525 20.4571 39.6032 20.4571 40.4087L20.4571 44.7773C20.4571 47.1891 22.4138 49.1458 24.8256 49.1458L38.7822 49.1458L35.4421 52.4859C34.8733 53.0548 34.8733 53.9785 35.4421 54.5473C36.0109 55.1162 36.9347 55.1162 37.5035 54.5473L43.3283 48.7226C43.8971 48.1538 43.8971 47.23 43.3283 46.6612L37.5035 40.8365C36.9347 40.2676 36.0109 40.2676 35.4421 40.8365C34.8733 41.4053 34.8733 42.329 35.4421 42.8979L38.7822 46.2334L24.8256 46.2334C24.0202 46.2334 23.3694 45.5827 23.3694 44.7773L23.3694 40.4087Z"
fill="#666666"
/>
</Icon>
);
}

server/sonar-web/src/main/js/apps/quality-profiles/components/__tests__/ProfileLink-test.tsx → server/sonar-web/src/main/js/components/icons/NewQualityProfileIcon.tsx Переглянути файл

@@ -17,23 +17,18 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen } from '@testing-library/react';
import * as React from 'react';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import ProfileLink from '../ProfileLink';
import Icon, { IconProps } from './Icon';

it('should be active when on the right path', () => {
renderProfileLink('/profiles/show');

expect(screen.getByRole('link')).toHaveClass('link-no-underline');
});

it('should be inactive when on a different path', () => {
renderProfileLink('/toto');

expect(screen.getByRole('link')).not.toHaveClass('link-no-underline');
});

function renderProfileLink(path: string) {
return renderComponent(<ProfileLink language="js" name="SonarWay" />, path);
export default function NewQualityProfileIcon({
...iconProps
}: Omit<IconProps, 'viewBox' | 'fill'>) {
return (
<Icon {...iconProps} viewBox="0 0 82 64">
<path
d="M28 18.4201C28 15.9787 29.9787 14 32.4201 14H43.8504C45.0245 14 46.1433 14.4657 46.9721 15.295L53.2293 21.5488C54.0581 22.3776 54.5209 23.4964 54.5209 24.6705V44.941C54.5209 47.379 52.5387 49.3612 50.1007 49.3612H32.4201C29.9787 49.3612 28 47.379 28 44.941V18.4201ZM51.2058 44.941V25.0504H45.6806C44.4581 25.0504 43.4705 24.0627 43.4705 22.8403V17.3151H32.4201C31.8096 17.3151 31.3151 17.8096 31.3151 18.4201V44.941C31.3151 45.5488 31.8096 46.046 32.4201 46.046H50.1007C50.7085 46.046 51.2058 45.5488 51.2058 44.941Z"
fill="#236A97"
/>
</Icon>
);
}

+ 1
- 1
server/sonar-web/src/main/js/helpers/l10n.ts Переглянути файл

@@ -43,7 +43,7 @@ export function translateWithParameters(
if (message) {
return parameters
.map((parameter) => String(parameter))
.reduce((acc, parameter, index) => acc.replace(`{${index}}`, () => parameter), message);
.reduce((acc, parameter, index) => acc.replaceAll(`{${index}}`, () => parameter), message);
}
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console

+ 31
- 13
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -1656,7 +1656,7 @@ project.info.see_more_info_on_x_locs=See more information on your {0} lines of c
#
#------------------------------------------------------------------------------

quality_profiles.new_profile=New Profile
quality_profiles.new_profile=New Quality Profile
quality_profiles.compare_with=Compare with
quality_profiles.filter_by=Filter profiles by
quality_profiles.restore_profile=Restore Profile
@@ -1689,7 +1689,7 @@ quality_profiles.changelog.DEACTIVATED=Deactivated
quality_profiles.changelog.UPDATED=Updated
quality_profiles.changelog.parameter_reset_to_default_value=Parameter {0} reset to default value
quality_profiles.deleted_profile=The profile {0} doesn't exist anymore
quality_profiles.projects_for_default=Every project not specifically associated with a Quality Profile will be associated to this one by default.
quality_profiles.projects_for_default=Every project not specifically associated with a quality profile will be associated to this one by default.
quality_profile.x_rules={0} rule(s)
quality_profile.x_active_rules={0} active rules
quality_profiles.x_overridden_rules={0} overridden rules
@@ -1697,29 +1697,29 @@ quality_profiles.change_parent=Change Parent
quality_profiles.all_profiles=All Profiles
quality_profiles.x_profiles={0} profile(s)
quality_profiles.x_Profiles={0} Profiles
quality_profiles.projects.select_hint=Click to associate this project with the Quality Profile
quality_profiles.projects.deselect_hint=Click to remove association between this project and the Quality Profile
quality_profile.empty_comparison=The Quality Profiles are equal.
quality_profiles.projects.select_hint=Click to associate this project with the quality profile
quality_profiles.projects.deselect_hint=Click to remove association between this project and the quality profile
quality_profile.empty_comparison=The quality profiles are equal.
quality_profiles.activate_more=Activate More
quality_profiles.activate_more.help.built_in=This Quality Profile is built in, and cannot be updated manually. If you want to activate more rules, create a new profile that inherits from this one and add rules there.
quality_profiles.activate_more.help.built_in=This quality profile is built in, and cannot be updated manually. If you want to activate more rules, create a new profile that inherits from this one and add rules there.
quality_profiles.activate_more_rules=Activate More Rules
quality_profiles.intro1=Quality Profiles are collections of rules to apply during an analysis.
quality_profiles.intro1=Quality profiles are collections of rules to apply during an analysis.
quality_profiles.intro2=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default. Ideally, all projects will use the same profile for a language.
quality_profiles.list.projects=Projects
quality_profiles.list.projects.help=Projects assigned to a profile will always be analyzed with it for that language, regardless of which profile is the default. Quality Profile administrators may assign projects to a non-default profile, or always make it follow the system default. Project administrators may choose any profile for each language.
quality_profiles.list.projects.help=Projects assigned to a profile will always be analyzed with it for that language, regardless of which profile is the default. Quality profile administrators may assign projects to a non-default profile, or always make it follow the system default. Project administrators may choose any profile for each language.
quality_profiles.list.rules=Rules
quality_profiles.list.updated=Updated
quality_profiles.list.used=Used
quality_profiles.list.default.help=For each language there is a default profile. All projects not explicitly assigned to some other profile will be analyzed with the default.
quality_profiles.x_updated_on_y={0}, updated on {1}
quality_profiles.change_projects=Change Projects
quality_profiles.not_found=The requested Quality Profile was not found.
quality_profiles.not_found=The requested quality profile was not found.
quality_profiles.latest_new_rules=Recently Added Rules
quality_profiles.latest_new_rules.activated={0}, activated on {1} profile(s)
quality_profiles.latest_new_rules.not_activated={0}, not yet activated
quality_profiles.deprecated_rules=Deprecated Rules
quality_profiles.deprecated_rules_description=These deprecated rules will eventually disappear. You should proactively investigate replacing them.
quality_profiles.deprecated_rules_are_still_activated=Deprecated rules are still activated on {0} Quality Profile(s):
quality_profiles.deprecated_rules_are_still_activated=Deprecated rules are still activated on {0} quality profile(s):
quality_profiles.sonarway_missing_rules=Sonar way rules not included
quality_profiles.sonarway_missing_rules_description=Recommended rules are missing from your profile
quality_profiles.stagnant_profiles=Stagnant Profiles
@@ -1729,9 +1729,9 @@ quality_profiles.exporters.deprecated=Exporters are deprecated, and will be remo
quality_profiles.updated_=Updated:
quality_profiles.used_=Used:
quality_profiles.built_in=Built-in
quality_profiles.built_in.description=This is a built-in Quality Profile that might be updated automatically.
quality_profiles.extends_built_in=Because this Quality Profile inherits from a built-in Quality Profile, it might be updated automatically.
quality_profiles.default_permissions=Users with the global "Administer Quality Profiles" permission and those listed below can manage this Quality Profile.
quality_profiles.built_in.description=This is a built-in quality profile that might be updated automatically.
quality_profiles.extends_built_in=Because this quality profile inherits from a built-in quality profile, it might be updated automatically.
quality_profiles.default_permissions=Users with the global "Administer Quality Profiles" permission and those listed below can manage this quality profile.
quality_profiles.grant_permissions_to_more_users=Grant permissions to more users
quality_profiles.grant_permissions_to_user_or_group=Grant permissions to a user or a group
quality_profiles.additional_user_groups=Additional users / groups:
@@ -1740,6 +1740,24 @@ quality_profiles.permissions.remove.user=Remove permission from user
quality_profiles.permissions.remove.user.confirmation=Are you sure you want to remove permission on this quality profile from user {user}?
quality_profiles.permissions.remove.group=Remove permission from group
quality_profiles.permissions.remove.group.confirmation=Are you sure you want to remove permission on this quality profile from group {user}?
quality_profiles.copy_help=Create a new quality qrofile as a replica of "{0}". The two profiles will then evolve independently.
quality_profiles.extend_help=Create a child quality qrofile inheriting all active rules from "{0}". Changes to "{0}" will impact the child profile.
quality_profiles.extend_description=This profile extends {link}.
quality_profiles.extend_description_help=Changes to "{0}" or any of its parents may impact this quality profile.
quality_profiles.chose_creation_type=What type of profile do you want to create?
quality_profiles.creation_from_extend=Extend an existing quality profile
quality_profiles.creation_from_extend_description_1=Create a child quality profile inheriting its parent’s active rules.
quality_profiles.creation_from_extend_description_2=Changes to the parent profile will impact the child profile.
quality_profiles.creation_from_copy=Copy an existing quality profile
quality_profiles.creation_from_copy_description_1=Create a new quality profile as a replica of the copied quality profile.
quality_profiles.creation_from_copy_description_2=The two profiles will then evolve independently.
quality_profiles.creation_from_blank=Create a blank quality profile
quality_profiles.creation_from_blank_description=Create a new quality profile with no active rules by default.
quality_profiles.creation.choose_parent_quality_profile=Profile to extend
quality_profiles.creation.choose_copy_quality_profile=Profile to copy
quality_profiles.name_invalid=Quality profile name should not be empty
quality_profiles.actions=Open {0} {1} quality profile actions





Завантаження…
Відмінити
Зберегти