Browse Source

SONAR-15144 autoscroll to audit category

tags/9.1.0.47736
Jeremy Davis 2 years ago
parent
commit
9750b27371

+ 6
- 1
server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx View File

@@ -87,7 +87,12 @@ export default function AuditAppRenderer(props: AuditAppRendererProps) {
values={{
housekeeping: translate('audit_logs.housekeeping_policy', housekeepingPolicy),
link: (
<Link to={{ pathname: '/admin/settings', query: { category: 'housekeeping' } }}>
<Link
to={{
pathname: '/admin/settings',
query: { category: 'housekeeping' },
hash: '#auditLogs'
}}>
{translate('audit_logs.page.description.link')}
</Link>
)

+ 4
- 0
server/sonar-web/src/main/js/apps/audit-logs/components/__tests__/__snapshots__/AuditAppRenderer-test.tsx.snap View File

@@ -38,6 +38,7 @@ exports[`should render correctly for Monthly housekeeping policy 1`] = `
style={Object {}}
to={
Object {
"hash": "#auditLogs",
"pathname": "/admin/settings",
"query": Object {
"category": "housekeeping",
@@ -162,6 +163,7 @@ exports[`should render correctly for Trimestrial housekeeping policy 1`] = `
style={Object {}}
to={
Object {
"hash": "#auditLogs",
"pathname": "/admin/settings",
"query": Object {
"category": "housekeeping",
@@ -298,6 +300,7 @@ exports[`should render correctly for Weekly housekeeping policy 1`] = `
style={Object {}}
to={
Object {
"hash": "#auditLogs",
"pathname": "/admin/settings",
"query": Object {
"category": "housekeeping",
@@ -410,6 +413,7 @@ exports[`should render correctly for Yearly housekeeping policy 1`] = `
style={Object {}}
to={
Object {
"hash": "#auditLogs",
"pathname": "/admin/settings",
"query": Object {
"category": "housekeeping",

+ 41
- 7
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx View File

@@ -19,36 +19,63 @@
*/
import { groupBy, isEqual, sortBy } from 'lodash';
import * as React from 'react';
import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
import { Location, withRouter } from '../../../components/hoc/withRouter';
import { sanitizeStringRestricted } from '../../../helpers/sanitize';
import { Setting, SettingCategoryDefinition } from '../../../types/settings';
import { SettingWithCategory } from '../../../types/settings';
import { getSubCategoryDescription, getSubCategoryName } from '../utils';
import DefinitionsList from './DefinitionsList';
import EmailForm from './EmailForm';

interface Props {
export interface SubCategoryDefinitionsListProps {
category: string;
component?: T.Component;
fetchValues: Function;
settings: Array<Setting & { definition: SettingCategoryDefinition }>;
location: Location;
settings: Array<SettingWithCategory>;
subCategory?: string;
}

export default class SubCategoryDefinitionsList extends React.PureComponent<Props> {
const SCROLL_OFFSET_TOP = 200;
const SCROLL_OFFSET_BOTTOM = 500;

export class SubCategoryDefinitionsList extends React.PureComponent<
SubCategoryDefinitionsListProps
> {
componentDidMount() {
this.fetchValues();
}

componentDidUpdate(prevProps: Props) {
componentDidUpdate(prevProps: SubCategoryDefinitionsListProps) {
const prevKeys = prevProps.settings.map(setting => setting.definition.key);
const keys = this.props.settings.map(setting => setting.definition.key);
if (prevProps.component !== this.props.component || !isEqual(prevKeys, keys)) {
this.fetchValues();
}

const { hash } = this.props.location;
if (hash && prevProps.location.hash !== hash) {
const element = document.querySelector<HTMLHeadingElement>(`h2[data-key=${hash.substr(1)}]`);
this.scrollToSubCategory(element);
}
}

scrollToSubCategory = (element: HTMLHeadingElement | null) => {
if (element) {
const { hash } = this.props.location;
if (hash && hash.substr(1) === element.getAttribute('data-key')) {
scrollToElement(element, {
topOffset: SCROLL_OFFSET_TOP,
bottomOffset: SCROLL_OFFSET_BOTTOM,
smooth: true
});
}
}
};

fetchValues() {
const keys = this.props.settings.map(setting => setting.definition.key).join();
this.props.fetchValues(keys, this.props.component && this.props.component.key);
return this.props.fetchValues(keys, this.props.component && this.props.component.key);
}

renderEmailForm = (subCategoryKey: string) => {
@@ -76,7 +103,12 @@ export default class SubCategoryDefinitionsList extends React.PureComponent<Prop
<ul className="settings-sub-categories-list">
{filteredSubCategories.map(subCategory => (
<li key={subCategory.key}>
<h2 className="settings-sub-category-name">{subCategory.name}</h2>
<h2
className="settings-sub-category-name"
data-key={subCategory.key}
ref={this.scrollToSubCategory}>
{subCategory.name}
</h2>
{subCategory.description != null && (
<div
className="settings-sub-category-description markdown"
@@ -97,3 +129,5 @@ export default class SubCategoryDefinitionsList extends React.PureComponent<Prop
);
}
}

export default withRouter(SubCategoryDefinitionsList);

+ 2
- 13
server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx View File

@@ -20,21 +20,10 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { Setting } from '../../../../types/settings';
import { mockSetting } from '../../../../helpers/mocks/settings';
import { Definition } from '../Definition';

const setting: Setting = {
key: 'foo',
value: '42',
inherited: true,
definition: {
key: 'foo',
name: 'Foo setting',
description: 'When Foo then Bar',
type: 'INTEGER',
options: []
}
};
const setting = mockSetting();

beforeAll(() => {
jest.useFakeTimers();

+ 82
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/SubCategoryDefinitionsList-test.tsx View File

@@ -0,0 +1,82 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { mount, shallow } from 'enzyme';
import * as React from 'react';
import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { mockSettingWithCategory } from '../../../../helpers/mocks/settings';
import { mockLocation } from '../../../../helpers/testMocks';
import {
SubCategoryDefinitionsList,
SubCategoryDefinitionsListProps
} from '../SubCategoryDefinitionsList';

jest.mock('sonar-ui-common/helpers/scrolling', () => ({
scrollToElement: jest.fn()
}));

it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('');
expect(shallowRender({ subCategory: 'qg' })).toMatchSnapshot('subcategory');
});

it('should scroll if hash is defined', async () => {
const wrapper = shallowRender({ location: mockLocation({ hash: '#qg' }) });

await waitAndUpdate(wrapper);

wrapper.find('h2').forEach(node => mount(node.getElement()));

expect(scrollToElement).toBeCalled();
});

it('should scroll when hash is updated', async () => {
const wrapper = shallowRender({ location: mockLocation({ hash: '#qg' }) });

wrapper.setProps({ location: mockLocation({ hash: '#email' }) });

await waitAndUpdate(wrapper);

expect(scrollToElement).toBeCalled();
});

function shallowRender(props: Partial<SubCategoryDefinitionsListProps> = {}) {
return shallow<SubCategoryDefinitionsListProps>(
<SubCategoryDefinitionsList
category="general"
fetchValues={jest.fn().mockResolvedValue({})}
location={mockLocation()}
settings={[
mockSettingWithCategory(),
mockSettingWithCategory({
definition: {
key: 'qg',
category: 'general',
subCategory: 'qg',
fields: [],
options: [],
description: 'awesome description'
}
})
]}
{...props}
/>
);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AnalysisScope-test.tsx.snap View File

@@ -48,7 +48,7 @@ exports[`should render correctly 1`] = `
<div
className="settings-sub-category"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="TEST"
component={
Object {

+ 2
- 2
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/AppContainer-test.tsx.snap View File

@@ -110,7 +110,7 @@ exports[`should render default view correctly 1`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="general"
/>
</div>
@@ -252,7 +252,7 @@ exports[`should render pull request decoration binding correctly 1`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="pull_request_decoration_binding"
/>
</div>

+ 1
- 1
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/Languages-test.tsx.snap View File

@@ -39,7 +39,7 @@ exports[`should render correctly 1`] = `
<div
className="settings-sub-category"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="java"
/>
</div>

+ 105
- 0
server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap View File

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

exports[`should render correctly 1`] = `
<ul
className="settings-sub-categories-list"
>
<li
key="email"
>
<h2
className="settings-sub-category-name"
data-key="email"
>
email
</h2>
<DefinitionsList
settings={
Array [
Object {
"definition": Object {
"category": "general",
"description": "When Foo then Bar",
"fields": Array [],
"key": "foo",
"name": "Foo setting",
"options": Array [],
"subCategory": "email",
"type": "INTEGER",
},
"inherited": true,
"key": "foo",
"value": "42",
},
]
}
/>
<Connect(withCurrentUser(EmailForm)) />
</li>
<li
key="qg"
>
<h2
className="settings-sub-category-name"
data-key="qg"
>
qg
</h2>
<DefinitionsList
settings={
Array [
Object {
"definition": Object {
"category": "general",
"description": "awesome description",
"fields": Array [],
"key": "qg",
"options": Array [],
"subCategory": "qg",
},
"inherited": true,
"key": "foo",
"value": "42",
},
]
}
/>
</li>
</ul>
`;

exports[`should render correctly: subcategory 1`] = `
<ul
className="settings-sub-categories-list"
>
<li
key="qg"
>
<h2
className="settings-sub-category-name"
data-key="qg"
>
qg
</h2>
<DefinitionsList
settings={
Array [
Object {
"definition": Object {
"category": "general",
"description": "awesome description",
"fields": Array [],
"key": "qg",
"options": Array [],
"subCategory": "qg",
},
"inherited": true,
"key": "foo",
"value": "42",
},
]
}
/>
</li>
</ul>
`;

+ 12
- 12
server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap View File

@@ -49,7 +49,7 @@ exports[`should render correctly for multi-ALM binding: editing a definition 1`]
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -106,7 +106,7 @@ exports[`should render correctly for multi-ALM binding: loaded 1`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -163,7 +163,7 @@ exports[`should render correctly for multi-ALM binding: loading ALM definitions
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -220,7 +220,7 @@ exports[`should render correctly for multi-ALM binding: loading project count 1`
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -277,7 +277,7 @@ exports[`should render correctly for single-ALM binding 1`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -334,7 +334,7 @@ exports[`should render correctly for single-ALM binding 2`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -391,7 +391,7 @@ exports[`should render correctly for single-ALM binding 3`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -438,7 +438,7 @@ exports[`should render correctly with validation: create a first 1`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -499,7 +499,7 @@ exports[`should render correctly with validation: create a second 1`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -560,7 +560,7 @@ exports[`should render correctly with validation: default 1`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -607,7 +607,7 @@ exports[`should render correctly with validation: empty 1`] = `
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="azure"
/>
@@ -666,7 +666,7 @@ exports[`should render correctly with validation: pass the correct key for bitbu
<div
className="big-padded"
>
<Connect(SubCategoryDefinitionsList)
<Connect(withRouter(SubCategoryDefinitionsList))
category="almintegration"
subCategory="bitbucket"
/>

+ 57
- 0
server/sonar-web/src/main/js/helpers/mocks/settings.ts View File

@@ -0,0 +1,57 @@
/*
* SonarQube
* Copyright (C) 2009-2021 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { Setting, SettingWithCategory } from '../../types/settings';

export function mockSetting(overrides: Partial<Setting> = {}): Setting {
return {
key: 'foo',
value: '42',
inherited: true,
definition: {
key: 'foo',
name: 'Foo setting',
description: 'When Foo then Bar',
type: 'INTEGER',
options: []
},
...overrides
};
}

export function mockSettingWithCategory(
overrides: Partial<SettingWithCategory> = {}
): SettingWithCategory {
return {
key: 'foo',
value: '42',
inherited: true,
definition: {
key: 'foo',
name: 'Foo setting',
description: 'When Foo then Bar',
type: 'INTEGER',
options: [],
category: 'general',
fields: [],
subCategory: 'email'
},
...overrides
};
}

+ 1
- 0
server/sonar-web/src/main/js/types/settings.ts View File

@@ -25,6 +25,7 @@ export const enum SettingsKey {
}

export type Setting = SettingValue & { definition: SettingDefinition };
export type SettingWithCategory = Setting & { definition: SettingCategoryDefinition };

export type SettingType =
| 'STRING'

Loading…
Cancel
Save