Browse Source

SONAR-12467 Improve back to rule list link

tags/8.2.0.32929
Wouter Admiraal 4 years ago
parent
commit
ec624ec45e

+ 45
- 14
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx View File

import { withRouter, WithRouterProps } from 'react-router'; import { withRouter, WithRouterProps } from 'react-router';
import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; 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 { translate } from 'sonar-ui-common/helpers/l10n';
import { import {
addSideBarClass, addSideBarClass,
getAppFacet, getAppFacet,
getOpen, getOpen,
getServerFacet, getServerFacet,
hasRuleKey,
OpenFacets, OpenFacets,
parseQuery, parseQuery,
Query, Query,
referencedRepositories: T.Dict<{ key: string; language: string; name: string }>; referencedRepositories: T.Dict<{ key: string; language: string; name: string }>;
rules: T.Rule[]; rules: T.Rule[];
selected?: string; selected?: string;
usingPermalink?: boolean;
} }


export class App extends React.PureComponent<Props, State> { export class App extends React.PureComponent<Props, State> {
const openRule = this.getOpenRule(nextProps, rules); const openRule = this.getOpenRule(nextProps, rules);
return { return {
openRule, openRule,
usingPermalink: hasRuleKey(nextProps.location.query),
query: parseQuery(nextProps.location.query), query: parseQuery(nextProps.location.query),
selected: openRule ? openRule.key : selected selected: openRule ? openRule.key : selected
}; };
return false; return false;
}); });
key('left', 'coding-rules', () => { key('left', 'coding-rules', () => {
this.closeRule();
this.handleBack();
return false; return false;
}); });
}; };
this.makeFetchRequest(query).then(({ actives, facets, paging, rules }) => { this.makeFetchRequest(query).then(({ actives, facets, paging, rules }) => {
if (this.mounted) { if (this.mounted) {
const openRule = this.getOpenRule(this.props, rules); 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; 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); }, this.stopLoading);
}; };
this.props.router.push(this.getRulePath(ruleKey)); 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>) => { handleFilterChange = (changes: Partial<Query>) => {
<div className="layout-page-main-inner"> <div className="layout-page-main-inner">
<A11ySkipTarget anchor="rules_main" /> <A11ySkipTarget anchor="rules_main" />
{this.state.openRule ? ( {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> </a>
) : ( ) : (
this.renderBulkButton() 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> </div>
</div> </div>

+ 39
- 20
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/App-test.tsx View File

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; 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 { import {
mockAppState, mockAppState,
mockCurrentUser, mockCurrentUser,
mockLocation, mockLocation,
mockOrganization, mockOrganization,
mockRouter
mockRouter,
mockRule
} from '../../../../helpers/testMocks'; } 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', () => ({ jest.mock('../../../../api/quality-profiles', () => ({
searchQualityProfiles: jest.fn().mockResolvedValue({ profiles: [] }) searchQualityProfiles: jest.fn().mockResolvedValue({ profiles: [] })


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


await waitAndUpdate(wrapper); 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', () => { describe('renderBulkButton', () => {

+ 420
- 43
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/App-test.tsx.snap View File

/> />
`; `;


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> <Fragment>
<Suggestions <Suggestions
suggestions="coding_rules" suggestions="coding_rules"
</Fragment> </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> <Fragment>
<Suggestions <Suggestions
suggestions="coding_rules" suggestions="coding_rules"
<A11ySkipTarget <A11ySkipTarget
anchor="rules_main" 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 <PageActions
loading={false} loading={false}
onReload={[Function]} onReload={[Function]}
"total": 0, "total": 0,
} }
} }
selectedIndex={0}
/> />
</div> </div>
</div> </div>
<div <div
className="layout-page-main-inner" 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>
</div> </div>

+ 4
- 0
server/sonar-web/src/main/js/apps/coding-rules/query.ts View File

return query.open; return query.open;
} }


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

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

+ 1
- 10
server/sonar-web/src/main/js/apps/documentation/components/__tests__/App-test.tsx View File

import getPages from '../../pages'; import getPages from '../../pages';
import App from '../App'; 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', () => ({ jest.mock('../../../../helpers/system', () => ({
isSonarCloud: jest.fn().mockReturnValue(false) isSonarCloud: jest.fn().mockReturnValue(false)

+ 2
- 11
server/sonar-web/src/main/js/apps/web-api/components/__tests__/WebApiApp-test.tsx View File

import { mockLocation, mockRouter } from '../../../../helpers/testMocks'; import { mockLocation, mockRouter } from '../../../../helpers/testMocks';
import { WebApiApp } from '../WebApiApp'; import { WebApiApp } from '../WebApiApp';


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

jest.mock('../../../../api/web-api', () => ({ jest.mock('../../../../api/web-api', () => ({
fetchWebApi: jest.fn().mockResolvedValue([ fetchWebApi: jest.fn().mockResolvedValue([
{ {
removeSideBarClass: jest.fn() 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 () => { it('should render correctly', async () => {
(global as any).scrollTo = jest.fn(); (global as any).scrollTo = jest.fn();



+ 30
- 0
server/sonar-web/src/main/js/components/common/__mocks__/ScreenPositionHelper.tsx View File

/*
* 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 View File

coding_rules.quality_profile=Quality Profile coding_rules.quality_profile=Quality Profile
coding_rules.reactivate=Reactivate 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.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=Remove Extended Description
coding_rules.remove_extended_description.confirm=Are you sure you want to remove the 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) coding_rules.repository_language=Rule repository (language)

Loading…
Cancel
Save