Kaynağa Gözat

SONAR-12467 Improve back to rule list link

tags/8.2.0.32929
Wouter Admiraal 4 yıl önce
ebeveyn
işleme
ec624ec45e

+ 45
- 14
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx Dosyayı Görüntüle

@@ -25,6 +25,7 @@ import { connect } from 'react-redux';
import { withRouter, WithRouterProps } from 'react-router';
import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
import SearchBox from 'sonar-ui-common/components/controls/SearchBox';
import BackIcon from 'sonar-ui-common/components/icons/BackIcon';
import { translate } from 'sonar-ui-common/helpers/l10n';
import {
addSideBarClass,
@@ -65,6 +66,7 @@ import {
getAppFacet,
getOpen,
getServerFacet,
hasRuleKey,
OpenFacets,
parseQuery,
Query,
@@ -107,6 +109,7 @@ interface State {
referencedRepositories: T.Dict<{ key: string; language: string; name: string }>;
rules: T.Rule[];
selected?: string;
usingPermalink?: boolean;
}

export class App extends React.PureComponent<Props, State> {
@@ -145,6 +148,7 @@ export class App extends React.PureComponent<Props, State> {
const openRule = this.getOpenRule(nextProps, rules);
return {
openRule,
usingPermalink: hasRuleKey(nextProps.location.query),
query: parseQuery(nextProps.location.query),
selected: openRule ? openRule.key : selected
};
@@ -187,7 +191,7 @@ export class App extends React.PureComponent<Props, State> {
return false;
});
key('left', 'coding-rules', () => {
this.closeRule();
this.handleBack();
return false;
});
};
@@ -271,8 +275,18 @@ export class App extends React.PureComponent<Props, State> {
this.makeFetchRequest(query).then(({ actives, facets, paging, rules }) => {
if (this.mounted) {
const openRule = this.getOpenRule(this.props, rules);
const usingPermalink = hasRuleKey(this.props.location.query);
const selected = rules.length > 0 ? (openRule && openRule.key) || rules[0].key : undefined;
this.setState({ actives, facets, loading: false, openRule, paging, rules, selected });
this.setState({
actives,
facets,
loading: false,
openRule,
paging,
rules,
selected,
usingPermalink
});
}
}, this.stopLoading);
};
@@ -422,10 +436,19 @@ export class App extends React.PureComponent<Props, State> {
this.props.router.push(this.getRulePath(ruleKey));
};

handleBack = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.closeRule();
handleBack = (event?: React.SyntheticEvent<HTMLAnchorElement>) => {
const { usingPermalink } = this.state;

if (event) {
event.preventDefault();
event.currentTarget.blur();
}

if (usingPermalink) {
this.handleReset();
} else {
this.closeRule();
}
};

handleFilterChange = (changes: Partial<Query>) => {
@@ -607,18 +630,26 @@ export class App extends React.PureComponent<Props, State> {
<div className="layout-page-main-inner">
<A11ySkipTarget anchor="rules_main" />
{this.state.openRule ? (
<a className="js-back" href="#" onClick={this.handleBack}>
{translate('coding_rules.return_to_list')}
<a
className="js-back display-inline-flex-center link-no-underline"
href="#"
onClick={this.handleBack}>
<BackIcon className="spacer-right" />
{this.state.usingPermalink
? translate('coding_rules.see_all')
: translate('coding_rules.return_to_list')}
</a>
) : (
this.renderBulkButton()
)}
<PageActions
loading={this.state.loading}
onReload={this.handleReload}
paging={paging}
selectedIndex={selectedIndex}
/>
{!this.state.usingPermalink && (
<PageActions
loading={this.state.loading}
onReload={this.handleReload}
paging={paging}
selectedIndex={selectedIndex}
/>
)}
</div>
</div>
</div>

+ 39
- 20
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx Dosyayı Görüntüle

@@ -17,33 +17,40 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { App } from '../App';
import { getRulesApp } from '../../../../api/rules';
import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
import { isSonarCloud } from '../../../../helpers/system';
import {
mockAppState,
mockCurrentUser,
mockLocation,
mockOrganization,
mockRouter
mockRouter,
mockRule
} from '../../../../helpers/testMocks';
import { getRulesApp } from '../../../../api/rules';
import { isSonarCloud } from '../../../../helpers/system';
import { App } from '../App';

jest.mock('../../../../api/rules', () => ({
getRulesApp: jest.fn().mockResolvedValue({ canWrite: true, repositories: [] }),
searchRules: jest.fn().mockResolvedValue({
actives: [],
rawActives: [],
facets: [],
rawFacets: [],
p: 0,
ps: 100,
rules: [],
total: 0
})
}));
jest.mock('../../../../components/common/ScreenPositionHelper');

jest.mock('../../../../api/rules', () => {
const { mockRule } = jest.requireActual('../../../../helpers/testMocks');
return {
getRulesApp: jest.fn().mockResolvedValue({ canWrite: true, repositories: [] }),
searchRules: jest.fn().mockResolvedValue({
actives: [],
rawActives: [],
facets: [],
rawFacets: [],
p: 0,
ps: 100,
rules: [mockRule(), mockRule()],
total: 0
})
};
});

jest.mock('../../../../api/quality-profiles', () => ({
searchQualityProfiles: jest.fn().mockResolvedValue({ profiles: [] })
@@ -55,10 +62,22 @@ jest.mock('../../../../helpers/system', () => ({

it('should render correctly', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(wrapper).toMatchSnapshot('loading');

await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();
expect(wrapper).toMatchSnapshot('loaded');
expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot(
'loaded (ScreenPositionHelper)'
);

wrapper.setState({ openRule: mockRule() });
expect(wrapper).toMatchSnapshot('open rule');
expect(wrapper.find(ScreenPositionHelper).dive()).toMatchSnapshot(
'open rule (ScreenPositionHelper)'
);

wrapper.setState({ usingPermalink: true });
expect(wrapper).toMatchSnapshot('using permalink');
});

describe('renderBulkButton', () => {

+ 420
- 43
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap Dosyayı Görüntüle

@@ -39,7 +39,249 @@ exports[`renderBulkButton should show bulk change button when everything is fine
/>
`;

exports[`should render correctly 1`] = `
exports[`should render correctly: loaded (ScreenPositionHelper) 1`] = `
<div
className="layout-page-side"
style={
Object {
"top": 0,
}
}
>
<div
className="layout-page-side-inner"
>
<div
className="layout-page-filters"
>
<A11ySkipTarget
anchor="rules_filters"
label="coding_rules.skip_to_filters"
weight={10}
/>
<FiltersHeader
displayReset={false}
onReset={[Function]}
/>
<SearchBox
className="spacer-bottom"
id="coding-rules-search"
minLength={2}
onChange={[Function]}
placeholder="search.search_for_rules"
value=""
/>
<FacetsList
facets={Object {}}
hideProfileFacet={false}
onFacetToggle={[Function]}
onFilterChange={[Function]}
openFacets={
Object {
"languages": true,
"owaspTop10": false,
"sansTop25": false,
"sonarsourceSecurity": false,
"standards": false,
"types": true,
}
}
organization="foo"
query={
Object {
"activation": undefined,
"activationSeverities": Array [],
"availableSince": undefined,
"compareToProfile": undefined,
"cwe": Array [],
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,
"sansTop25": Array [],
"searchQuery": undefined,
"severities": Array [],
"sonarsourceSecurity": Array [],
"statuses": Array [],
"tags": Array [],
"template": undefined,
"types": Array [],
}
}
referencedProfiles={Object {}}
referencedRepositories={Object {}}
/>
</div>
</div>
</div>
`;

exports[`should render correctly: loaded 1`] = `
<Fragment>
<Suggestions
suggestions="coding_rules"
/>
<Helmet
defer={false}
encodeSpecialCharacters={true}
title="coding_rules.page"
>
<meta
content="noindex"
name="robots"
/>
</Helmet>
<div
className="layout-page"
id="coding-rules-page"
>
<ScreenPositionHelper
className="layout-page-side-outer"
>
<Component />
</ScreenPositionHelper>
<div
className="layout-page-main"
>
<div
className="layout-page-header-panel layout-page-main-header"
>
<div
className="layout-page-header-panel-inner layout-page-main-header-inner"
>
<div
className="layout-page-main-inner"
>
<A11ySkipTarget
anchor="rules_main"
/>
<BulkChange
languages={
Object {
"js": Object {
"key": "js",
"name": "JavaScript",
},
}
}
organization="foo"
query={
Object {
"activation": undefined,
"activationSeverities": Array [],
"availableSince": undefined,
"compareToProfile": undefined,
"cwe": Array [],
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,
"sansTop25": Array [],
"searchQuery": undefined,
"severities": Array [],
"sonarsourceSecurity": Array [],
"statuses": Array [],
"tags": Array [],
"template": undefined,
"types": Array [],
}
}
referencedProfiles={Object {}}
total={0}
/>
<PageActions
loading={false}
onReload={[Function]}
paging={
Object {
"pageIndex": 0,
"pageSize": 100,
"total": 0,
}
}
selectedIndex={0}
/>
</div>
</div>
</div>
<div
className="layout-page-main-inner"
>
<RuleListItem
canWrite={true}
isLoggedIn={true}
key="javascript:S1067"
onActivate={[Function]}
onDeactivate={[Function]}
onFilterChange={[Function]}
onOpen={[Function]}
organization="foo"
rule={
Object {
"key": "javascript:S1067",
"lang": "js",
"langName": "JavaScript",
"name": "Use foo",
"severity": "MAJOR",
"status": "READY",
"sysTags": Array [
"a",
"b",
],
"tags": Array [
"x",
],
"type": "CODE_SMELL",
}
}
selected={true}
/>
<RuleListItem
canWrite={true}
isLoggedIn={true}
key="javascript:S1067"
onActivate={[Function]}
onDeactivate={[Function]}
onFilterChange={[Function]}
onOpen={[Function]}
organization="foo"
rule={
Object {
"key": "javascript:S1067",
"lang": "js",
"langName": "JavaScript",
"name": "Use foo",
"severity": "MAJOR",
"status": "READY",
"sysTags": Array [
"a",
"b",
],
"tags": Array [
"x",
],
"type": "CODE_SMELL",
}
}
selected={true}
/>
<ListFooter
count={2}
loadMore={[Function]}
ready={true}
total={0}
/>
</div>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: loading 1`] = `
<Fragment>
<Suggestions
suggestions="coding_rules"
@@ -93,7 +335,86 @@ exports[`should render correctly 1`] = `
</Fragment>
`;

exports[`should render correctly 2`] = `
exports[`should render correctly: open rule (ScreenPositionHelper) 1`] = `
<div
className="layout-page-side"
style={
Object {
"top": 0,
}
}
>
<div
className="layout-page-side-inner"
>
<div
className="layout-page-filters"
>
<A11ySkipTarget
anchor="rules_filters"
label="coding_rules.skip_to_filters"
weight={10}
/>
<FiltersHeader
displayReset={false}
onReset={[Function]}
/>
<SearchBox
className="spacer-bottom"
id="coding-rules-search"
minLength={2}
onChange={[Function]}
placeholder="search.search_for_rules"
value=""
/>
<FacetsList
facets={Object {}}
hideProfileFacet={false}
onFacetToggle={[Function]}
onFilterChange={[Function]}
openFacets={
Object {
"languages": true,
"owaspTop10": false,
"sansTop25": false,
"sonarsourceSecurity": false,
"standards": false,
"types": true,
}
}
organization="foo"
query={
Object {
"activation": undefined,
"activationSeverities": Array [],
"availableSince": undefined,
"compareToProfile": undefined,
"cwe": Array [],
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,
"sansTop25": Array [],
"searchQuery": undefined,
"severities": Array [],
"sonarsourceSecurity": Array [],
"statuses": Array [],
"tags": Array [],
"template": undefined,
"types": Array [],
}
}
referencedProfiles={Object {}}
referencedRepositories={Object {}}
/>
</div>
</div>
</div>
`;

exports[`should render correctly: open rule 1`] = `
<Fragment>
<Suggestions
suggestions="coding_rules"
@@ -132,42 +453,16 @@ exports[`should render correctly 2`] = `
<A11ySkipTarget
anchor="rules_main"
/>
<BulkChange
languages={
Object {
"js": Object {
"key": "js",
"name": "JavaScript",
},
}
}
organization="foo"
query={
Object {
"activation": undefined,
"activationSeverities": Array [],
"availableSince": undefined,
"compareToProfile": undefined,
"cwe": Array [],
"inheritance": undefined,
"languages": Array [],
"owaspTop10": Array [],
"profile": undefined,
"repositories": Array [],
"ruleKey": undefined,
"sansTop25": Array [],
"searchQuery": undefined,
"severities": Array [],
"sonarsourceSecurity": Array [],
"statuses": Array [],
"tags": Array [],
"template": undefined,
"types": Array [],
}
}
referencedProfiles={Object {}}
total={0}
/>
<a
className="js-back display-inline-flex-center link-no-underline"
href="#"
onClick={[Function]}
>
<BackIcon
className="spacer-right"
/>
coding_rules.return_to_list
</a>
<PageActions
loading={false}
onReload={[Function]}
@@ -178,6 +473,7 @@ exports[`should render correctly 2`] = `
"total": 0,
}
}
selectedIndex={0}
/>
</div>
</div>
@@ -185,11 +481,92 @@ exports[`should render correctly 2`] = `
<div
className="layout-page-main-inner"
>
<ListFooter
count={0}
loadMore={[Function]}
ready={true}
total={0}
<RuleDetails
allowCustomRules={true}
canWrite={true}
hideQualityProfiles={false}
onActivate={[Function]}
onDeactivate={[Function]}
onDelete={[Function]}
onFilterChange={[Function]}
organization="foo"
referencedProfiles={Object {}}
referencedRepositories={Object {}}
ruleKey="javascript:S1067"
/>
</div>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: using permalink 1`] = `
<Fragment>
<Suggestions
suggestions="coding_rules"
/>
<Helmet
defer={false}
encodeSpecialCharacters={true}
title="coding_rules.page"
>
<meta
content="noindex"
name="robots"
/>
</Helmet>
<div
className="layout-page"
id="coding-rules-page"
>
<ScreenPositionHelper
className="layout-page-side-outer"
>
<Component />
</ScreenPositionHelper>
<div
className="layout-page-main"
>
<div
className="layout-page-header-panel layout-page-main-header"
>
<div
className="layout-page-header-panel-inner layout-page-main-header-inner"
>
<div
className="layout-page-main-inner"
>
<A11ySkipTarget
anchor="rules_main"
/>
<a
className="js-back display-inline-flex-center link-no-underline"
href="#"
onClick={[Function]}
>
<BackIcon
className="spacer-right"
/>
coding_rules.see_all
</a>
</div>
</div>
</div>
<div
className="layout-page-main-inner"
>
<RuleDetails
allowCustomRules={true}
canWrite={true}
hideQualityProfiles={false}
onActivate={[Function]}
onDeactivate={[Function]}
onDelete={[Function]}
onFilterChange={[Function]}
organization="foo"
referencedProfiles={Object {}}
referencedRepositories={Object {}}
ruleKey="javascript:S1067"
/>
</div>
</div>

+ 4
- 0
server/sonar-web/src/main/js/apps/coding-rules/query.ts Dosyayı Görüntüle

@@ -156,6 +156,10 @@ export function getOpen(query: T.RawQuery) {
return query.open;
}

export function hasRuleKey(query: T.RawQuery) {
return Boolean(query.rule_key);
}

function parseAsInheritance(value?: string): T.RuleInheritance | undefined {
if (value === 'INHERITED' || value === 'NONE' || value === 'OVERRIDES') {
return value;

+ 1
- 10
server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx Dosyayı Görüntüle

@@ -26,16 +26,7 @@ import { isSonarCloud } from '../../../../helpers/system';
import getPages from '../../pages';
import App from '../App';

jest.mock('../../../../components/common/ScreenPositionHelper', () => ({
default: class ScreenPositionHelper extends React.Component<{
children: (pos: { top: number }) => React.ReactNode;
}> {
static displayName = 'ScreenPositionHelper';
render() {
return this.props.children({ top: 0 });
}
}
}));
jest.mock('../../../../components/common/ScreenPositionHelper');

jest.mock('../../../../helpers/system', () => ({
isSonarCloud: jest.fn().mockReturnValue(false)

+ 2
- 11
server/sonar-web/src/main/js/apps/web-api/components/__tests__/WebApiApp-test.tsx Dosyayı Görüntüle

@@ -25,6 +25,8 @@ import { fetchWebApi } from '../../../../api/web-api';
import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import { WebApiApp } from '../WebApiApp';

jest.mock('../../../../components/common/ScreenPositionHelper');

jest.mock('../../../../api/web-api', () => ({
fetchWebApi: jest.fn().mockResolvedValue([
{
@@ -42,17 +44,6 @@ jest.mock('sonar-ui-common/helpers/pages', () => ({
removeSideBarClass: jest.fn()
}));

jest.mock('../../../../components/common/ScreenPositionHelper', () => ({
default: class ScreenPositionHelper extends React.Component<{
children: (pos: { top: number }) => React.ReactNode;
}> {
static displayName = 'ScreenPositionHelper';
render() {
return this.props.children({ top: 0 });
}
}
}));

it('should render correctly', async () => {
(global as any).scrollTo = jest.fn();


+ 30
- 0
server/sonar-web/src/main/js/components/common/__mocks__/ScreenPositionHelper.tsx Dosyayı Görüntüle

@@ -0,0 +1,30 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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';

interface Props {
children: (position: { top: number }) => React.ReactNode;
}

export default class ScreenPositionHelper extends React.Component<Props> {
render() {
return this.props.children({ top: 0 });
}
}

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Dosyayı Görüntüle

@@ -1567,7 +1567,8 @@ coding_rules.quality_profiles.template_caption=This rule template was activated
coding_rules.quality_profile=Quality Profile
coding_rules.reactivate=Reactivate
coding_rules.reactivate.help=A rule with the same key has been previously deleted. Please reactivate the existing rule or modify the key to create a new rule.
coding_rules.return_to_list=Return to List
coding_rules.return_to_list=Return to list
coding_rules.see_all=See all rules
coding_rules.remove_extended_description=Remove Extended Description
coding_rules.remove_extended_description.confirm=Are you sure you want to remove the extended description?
coding_rules.repository_language=Rule repository (language)

Loading…
İptal
Kaydet