@@ -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> | |||
) |
@@ -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", |
@@ -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); |
@@ -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(); |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -48,7 +48,7 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="settings-sub-category" | |||
> | |||
<Connect(SubCategoryDefinitionsList) | |||
<Connect(withRouter(SubCategoryDefinitionsList)) | |||
category="TEST" | |||
component={ | |||
Object { |
@@ -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> |
@@ -39,7 +39,7 @@ exports[`should render correctly 1`] = ` | |||
<div | |||
className="settings-sub-category" | |||
> | |||
<Connect(SubCategoryDefinitionsList) | |||
<Connect(withRouter(SubCategoryDefinitionsList)) | |||
category="java" | |||
/> | |||
</div> |
@@ -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" | |||
> | |||
</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> | |||
`; |
@@ -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" | |||
/> |
@@ -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 | |||
}; | |||
} |
@@ -25,6 +25,7 @@ export const enum SettingsKey { | |||
} | |||
export type Setting = SettingValue & { definition: SettingDefinition }; | |||
export type SettingWithCategory = Setting & { definition: SettingCategoryDefinition }; | |||
export type SettingType = | |||
| 'STRING' |