aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main
diff options
context:
space:
mode:
authorJay <jeremy.davis@sonarsource.com>2021-02-24 14:19:10 +0100
committersonartech <sonartech@sonarsource.com>2021-02-26 20:07:40 +0000
commit8a34c0093e84fbdc50afd2d864db7f9ced8d6e46 (patch)
tree5ff886122cad12545c5005bcb0b81caccb12943c /server/sonar-web/src/main
parent10fef4f93aeefcf8d79e7eb93742d4460c75d90b (diff)
downloadsonarqube-8a34c0093e84fbdc50afd2d864db7f9ced8d6e46.tar.gz
sonarqube-8a34c0093e84fbdc50afd2d864db7f9ced8d6e46.zip
SONAR-14498 Support JSON field type in Settings
Diffstat (limited to 'server/sonar-web/src/main')
-rw-r--r--server/sonar-web/src/main/js/api/settings.ts7
-rw-r--r--server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts44
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/Definition.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts96
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/actions.ts9
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/definitions.ts5
-rw-r--r--server/sonar-web/src/main/js/apps/settings/store/values.ts9
-rw-r--r--server/sonar-web/src/main/js/apps/settings/utils.ts23
-rw-r--r--server/sonar-web/src/main/js/types/settings.ts49
-rw-r--r--server/sonar-web/src/main/js/types/types.d.ts48
19 files changed, 259 insertions, 106 deletions
diff --git a/server/sonar-web/src/main/js/api/settings.ts b/server/sonar-web/src/main/js/api/settings.ts
index 2e8019d730a..03946b15832 100644
--- a/server/sonar-web/src/main/js/api/settings.ts
+++ b/server/sonar-web/src/main/js/api/settings.ts
@@ -22,8 +22,9 @@ import { getJSON, post, postJSON, RequestData } from 'sonar-ui-common/helpers/re
import throwGlobalError from '../app/utils/throwGlobalError';
import { isCategoryDefinition } from '../apps/settings/utils';
import { BranchParameters } from '../types/branch-like';
+import { SettingCategoryDefinition, SettingDefinition, SettingValue } from '../types/settings';
-export function getDefinitions(component?: string): Promise<T.SettingCategoryDefinition[]> {
+export function getDefinitions(component?: string): Promise<SettingCategoryDefinition[]> {
return getJSON('/api/settings/list_definitions', { component }).then(
r => r.definitions,
throwGlobalError
@@ -32,12 +33,12 @@ export function getDefinitions(component?: string): Promise<T.SettingCategoryDef
export function getValues(
data: { keys: string; component?: string } & BranchParameters
-): Promise<T.SettingValue[]> {
+): Promise<SettingValue[]> {
return getJSON('/api/settings/values', data).then(r => r.settings);
}
export function setSettingValue(
- definition: T.SettingDefinition,
+ definition: SettingDefinition,
value: any,
component?: string
): Promise<void> {
diff --git a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts
index 8705c57668c..d37c7753bc4 100644
--- a/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts
+++ b/server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts
@@ -17,14 +17,19 @@
* 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,
+ SettingCategoryDefinition,
+ SettingFieldDefinition
+} from '../../../types/settings';
import { getDefaultValue, getEmptyValue, sanitizeTranslation } from '../utils';
const fields = [
- { key: 'foo', type: 'STRING' } as T.SettingFieldDefinition,
- { key: 'bar', type: 'SINGLE_SELECT_LIST' } as T.SettingFieldDefinition
+ { key: 'foo', type: 'STRING' } as SettingFieldDefinition,
+ { key: 'bar', type: 'SINGLE_SELECT_LIST' } as SettingFieldDefinition
];
-const settingDefinition: T.SettingCategoryDefinition = {
+const settingDefinition: SettingCategoryDefinition = {
category: 'test',
fields: [],
key: 'test',
@@ -34,7 +39,7 @@ const settingDefinition: T.SettingCategoryDefinition = {
describe('#getEmptyValue()', () => {
it('should work for property sets', () => {
- const setting: T.SettingCategoryDefinition = {
+ const setting: SettingCategoryDefinition = {
...settingDefinition,
type: 'PROPERTY_SET',
fields
@@ -43,7 +48,7 @@ describe('#getEmptyValue()', () => {
});
it('should work for multi values string', () => {
- const setting: T.SettingCategoryDefinition = {
+ const setting: SettingCategoryDefinition = {
...settingDefinition,
type: 'STRING',
multiValues: true
@@ -52,7 +57,7 @@ describe('#getEmptyValue()', () => {
});
it('should work for multi values boolean', () => {
- const setting: T.SettingCategoryDefinition = {
+ const setting: SettingCategoryDefinition = {
...settingDefinition,
type: 'BOOLEAN',
multiValues: true
@@ -62,19 +67,20 @@ describe('#getEmptyValue()', () => {
});
describe('#getDefaultValue()', () => {
- const check = (parentValue?: string, expected?: string) => {
- const setting: T.Setting = {
- definition: { key: 'test', options: [], type: 'BOOLEAN' },
- parentValue,
- key: 'test'
- };
- expect(getDefaultValue(setting)).toEqual(expected);
- };
-
- it('should work for boolean field when passing "true"', () =>
- check('true', 'settings.boolean.true'));
- it('should work for boolean field when passing "false"', () =>
- check('false', 'settings.boolean.false'));
+ it.each([
+ ['true', 'settings.boolean.true'],
+ ['false', 'settings.boolean.false']
+ ])(
+ 'should work for boolean field when passing "%s"',
+ (parentValue?: string, expected?: string) => {
+ const setting: Setting = {
+ definition: { key: 'test', options: [], type: 'BOOLEAN' },
+ parentValue,
+ key: 'test'
+ };
+ expect(getDefaultValue(setting)).toEqual(expected);
+ }
+ );
});
describe('sanitizeTranslation', () => {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
index 9b1036ce68e..66a0d15c5f9 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx
@@ -29,6 +29,7 @@ import {
isSettingsAppLoading,
Store
} from '../../../store/rootReducer';
+import { Setting } from '../../../types/settings';
import { checkValue, resetValue, saveValue } from '../store/actions';
import { cancelChange, changeValue, passValidation } from '../store/settingsPage';
import {
@@ -51,7 +52,7 @@ interface Props {
passValidation: (key: string) => void;
resetValue: (key: string, component?: string) => Promise<void>;
saveValue: (key: string, component?: string) => Promise<void>;
- setting: T.Setting;
+ setting: Setting;
validationMessage?: string;
}
@@ -59,6 +60,8 @@ interface State {
success: boolean;
}
+const SAFE_SET_STATE_DELAY = 3000;
+
export class Definition extends React.PureComponent<Props, State> {
timeout?: number;
mounted = false;
@@ -91,7 +94,10 @@ export class Definition extends React.PureComponent<Props, State> {
return this.props.resetValue(definition.key, componentKey).then(() => {
this.props.cancelChange(definition.key);
this.safeSetState({ success: true });
- this.timeout = window.setTimeout(() => this.safeSetState({ success: false }), 3000);
+ this.timeout = window.setTimeout(
+ () => this.safeSetState({ success: false }),
+ SAFE_SET_STATE_DELAY
+ );
});
};
@@ -113,9 +119,14 @@ export class Definition extends React.PureComponent<Props, State> {
this.props.saveValue(setting.definition.key, component && component.key).then(
() => {
this.safeSetState({ success: true });
- this.timeout = window.setTimeout(() => this.safeSetState({ success: false }), 3000);
+ this.timeout = window.setTimeout(
+ () => this.safeSetState({ success: false }),
+ SAFE_SET_STATE_DELAY
+ );
},
- () => {}
+ () => {
+ /* Do nothing */
+ }
);
}
};
diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
index cfc9eb5e71b..70eb44829c5 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
@@ -21,6 +21,7 @@ import * as React from 'react';
import { Button, ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import { translate } from 'sonar-ui-common/helpers/l10n';
+import { Setting } from '../../../types/settings';
import { getDefaultValue, getSettingValue, isEmptyValue } from '../utils';
type Props = {
@@ -31,7 +32,7 @@ type Props = {
onCancel: () => void;
onReset: () => void;
onSave: () => void;
- setting: T.Setting;
+ setting: Setting;
};
type State = { reseting: boolean };
diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx
index 6687de632cf..653cf225f8d 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx
@@ -18,11 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { Setting } from '../../../types/settings';
import Definition from './Definition';
interface Props {
component?: T.Component;
- settings: T.Setting[];
+ settings: Setting[];
}
export default function DefinitionsList({ component, settings }: Props) {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
index 1cc41a49a1d..22493ed1046 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
@@ -19,6 +19,7 @@
*/
import { groupBy, isEqual, sortBy } from 'lodash';
import * as React from 'react';
+import { Setting, SettingCategoryDefinition } from '../../../types/settings';
import { getSubCategoryDescription, getSubCategoryName, sanitizeTranslation } from '../utils';
import DefinitionsList from './DefinitionsList';
import EmailForm from './EmailForm';
@@ -27,7 +28,7 @@ interface Props {
category: string;
component?: T.Component;
fetchValues: Function;
- settings: Array<T.Setting & { definition: T.SettingCategoryDefinition }>;
+ settings: Array<Setting & { definition: SettingCategoryDefinition }>;
subCategory?: string;
}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx
index 8707c5f3e10..764be721a38 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/Definition-test.tsx
@@ -20,9 +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 { Definition } from '../Definition';
-const setting: T.Setting = {
+const setting: Setting = {
key: 'foo',
value: '42',
inherited: true,
@@ -35,6 +36,14 @@ const setting: T.Setting = {
}
};
+beforeAll(() => {
+ jest.useFakeTimers();
+});
+
+afterAll(() => {
+ jest.useRealTimers();
+});
+
it('should render correctly', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
@@ -65,10 +74,26 @@ it('should correctly save value change', async () => {
await waitAndUpdate(wrapper);
expect(saveValue).toHaveBeenCalledWith(setting.definition.key, undefined);
expect(wrapper.find('AlertSuccessIcon').exists()).toBe(true);
+ expect(wrapper.state().success).toBe(true);
+ jest.runAllTimers();
+ expect(wrapper.state().success).toBe(false);
+});
+
+it('should correctly reset', async () => {
+ const cancelChange = jest.fn();
+ const resetValue = jest.fn().mockResolvedValue({});
+ const wrapper = shallowRender({ cancelChange, changedValue: 10, resetValue });
+ wrapper.find('DefinitionActions').prop<Function>('onReset')();
+ await waitAndUpdate(wrapper);
+ expect(resetValue).toHaveBeenCalledWith(setting.definition.key, undefined);
+ expect(cancelChange).toHaveBeenCalledWith(setting.definition.key);
+ expect(wrapper.state().success).toBe(true);
+ jest.runAllTimers();
+ expect(wrapper.state().success).toBe(false);
});
function shallowRender(props: Partial<Definition['props']> = {}) {
- return shallow(
+ return shallow<Definition>(
<Definition
cancelChange={jest.fn()}
changeValue={jest.fn()}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx
index 7472ad0bcb7..76efb7f237d 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/DefinitionActions-test.tsx
@@ -19,9 +19,10 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { SettingCategoryDefinition } from '../../../../types/settings';
import DefinitionActions from '../DefinitionActions';
-const definition: T.SettingCategoryDefinition = {
+const definition: SettingCategoryDefinition = {
category: 'baz',
description: 'lorem',
fields: [],
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx
index 774426fdbb5..117b3916715 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx
@@ -19,9 +19,10 @@
*/
import * as React from 'react';
import Select from 'sonar-ui-common/components/controls/Select';
+import { SettingCategoryDefinition } from '../../../../types/settings';
import { DefaultSpecializedInputProps } from '../../utils';
-type Props = DefaultSpecializedInputProps & Pick<T.SettingCategoryDefinition, 'options'>;
+type Props = DefaultSpecializedInputProps & Pick<SettingCategoryDefinition, 'options'>;
export default class InputForSingleSelectList extends React.PureComponent<Props> {
handleInputChange = ({ value }: { value: string }) => {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx
index 44b6131c21d..ce91fc76318 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { SettingType } from '../../../../types/settings';
import {
DefaultInputProps,
DefaultSpecializedInputProps,
@@ -32,10 +33,11 @@ import InputForString from './InputForString';
import InputForText from './InputForText';
const typeMapping: {
- [type in T.SettingType]?: React.ComponentType<DefaultSpecializedInputProps>;
+ [type in SettingType]?: React.ComponentType<DefaultSpecializedInputProps>;
} = {
STRING: InputForString,
TEXT: InputForText,
+ JSON: InputForText,
PASSWORD: InputForPassword,
BOOLEAN: InputForBoolean,
INTEGER: InputForNumber,
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx
index 8623863c719..c800f03dee0 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/Input-test.tsx
@@ -19,6 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
+import { Setting, SettingCategoryDefinition } from '../../../../../types/settings';
import { DefaultInputProps } from '../../../utils';
import Input from '../Input';
@@ -26,7 +27,7 @@ const settingValue = {
key: 'example'
};
-const settingDefinition: T.SettingCategoryDefinition = {
+const settingDefinition: SettingCategoryDefinition = {
category: 'general',
fields: [],
key: 'example',
@@ -57,7 +58,7 @@ it('should render MultiValueInput', () => {
});
it('should render PropertySetInput', () => {
- const setting: T.Setting = {
+ const setting: Setting = {
...settingValue,
definition: { ...settingDefinition, type: 'PROPERTY_SET' }
};
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx
index cd90e9b0d0f..c47d92eadb2 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/MultiValueInput-test.tsx
@@ -20,6 +20,7 @@
import { shallow, ShallowWrapper } from 'enzyme';
import * as React from 'react';
import { click } from 'sonar-ui-common/helpers/testUtils';
+import { SettingCategoryDefinition } from '../../../../../types/settings';
import { DefaultInputProps } from '../../../utils';
import MultiValueInput from '../MultiValueInput';
import PrimitiveInput from '../PrimitiveInput';
@@ -28,7 +29,7 @@ const settingValue = {
key: 'example'
};
-const settingDefinition: T.SettingCategoryDefinition = {
+const settingDefinition: SettingCategoryDefinition = {
category: 'general',
fields: [],
key: 'example',
diff --git a/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts b/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts
index 68cf95eb1c4..924aa9190a1 100644
--- a/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts
+++ b/server/sonar-web/src/main/js/apps/settings/store/__tests__/actions-test.ts
@@ -17,7 +17,11 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { fetchSettings } from '../actions';
+import {
+ getSettingsAppChangedValue,
+ getSettingsAppDefinition
+} from '../../../../store/rootReducer';
+import { checkValue, fetchSettings } from '../actions';
import { receiveDefinitions } from '../definitions';
jest.mock('../../../../api/settings', () => ({
@@ -37,6 +41,11 @@ jest.mock('../definitions', () => ({
receiveDefinitions: jest.fn()
}));
+jest.mock('../../../../store/rootReducer', () => ({
+ getSettingsAppDefinition: jest.fn(),
+ getSettingsAppChangedValue: jest.fn()
+}));
+
it('#fetchSettings should filter LICENSE type settings', async () => {
const dispatch = jest.fn();
@@ -49,3 +58,88 @@ it('#fetchSettings should filter LICENSE type settings', async () => {
}
]);
});
+
+describe('checkValue', () => {
+ const dispatch = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should correctly identify empty strings', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ defaultValue: 'hello',
+ type: 'TEXT'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue(undefined);
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/FAIL_VALIDATION',
+ key,
+ message: 'settings.state.value_cant_be_empty'
+ });
+ });
+
+ it('should correctly identify empty with no default', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ type: 'TEXT'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue(undefined);
+
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/FAIL_VALIDATION',
+ key,
+ message: 'settings.state.value_cant_be_empty_no_default'
+ });
+ });
+
+ it('should correctly identify non-empty strings', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ type: 'TEXT'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue('not empty');
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(true);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/PASS_VALIDATION',
+ key
+ });
+ });
+
+ it('should correctly identify misformed JSON', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ type: 'JSON'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue('{JSON: "asd;{');
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(false);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/FAIL_VALIDATION',
+ key,
+ message: 'Unexpected token J in JSON at position 1'
+ });
+ });
+
+ it('should correctly identify correct JSON', () => {
+ (getSettingsAppDefinition as jest.Mock).mockReturnValue({
+ type: 'JSON'
+ });
+
+ (getSettingsAppChangedValue as jest.Mock).mockReturnValue(
+ '{"number": 42, "question": "answer"}'
+ );
+ const key = 'key';
+ expect(checkValue(key)(dispatch, jest.fn())).toBe(true);
+ expect(dispatch).toBeCalledWith({
+ type: 'settingsPage/PASS_VALIDATION',
+ key
+ });
+ });
+});
diff --git a/server/sonar-web/src/main/js/apps/settings/store/actions.ts b/server/sonar-web/src/main/js/apps/settings/store/actions.ts
index 23b969a9484..7201065e6ab 100644
--- a/server/sonar-web/src/main/js/apps/settings/store/actions.ts
+++ b/server/sonar-web/src/main/js/apps/settings/store/actions.ts
@@ -75,6 +75,15 @@ export function checkValue(key: string) {
return false;
}
+ if (definition.type === 'JSON') {
+ try {
+ JSON.parse(value);
+ } catch (e) {
+ dispatch(failValidation(key, e.message));
+ return false;
+ }
+ }
+
dispatch(passValidation(key));
return true;
};
diff --git a/server/sonar-web/src/main/js/apps/settings/store/definitions.ts b/server/sonar-web/src/main/js/apps/settings/store/definitions.ts
index 977b5ddf9f5..8e1af16c1db 100644
--- a/server/sonar-web/src/main/js/apps/settings/store/definitions.ts
+++ b/server/sonar-web/src/main/js/apps/settings/store/definitions.ts
@@ -19,6 +19,7 @@
*/
import { keyBy, sortBy, uniqBy } from 'lodash';
import { ActionType } from '../../../store/utils/actions';
+import { SettingCategoryDefinition } from '../../../types/settings';
import { DEFAULT_CATEGORY, getCategoryName } from '../utils';
const enum Actions {
@@ -27,9 +28,9 @@ const enum Actions {
type Action = ActionType<typeof receiveDefinitions, Actions.ReceiveDefinitions>;
-export type State = T.Dict<T.SettingCategoryDefinition>;
+export type State = T.Dict<SettingCategoryDefinition>;
-export function receiveDefinitions(definitions: T.SettingCategoryDefinition[]) {
+export function receiveDefinitions(definitions: SettingCategoryDefinition[]) {
return { type: Actions.ReceiveDefinitions, definitions };
}
diff --git a/server/sonar-web/src/main/js/apps/settings/store/values.ts b/server/sonar-web/src/main/js/apps/settings/store/values.ts
index 2a4256c1919..8b00453b9b6 100644
--- a/server/sonar-web/src/main/js/apps/settings/store/values.ts
+++ b/server/sonar-web/src/main/js/apps/settings/store/values.ts
@@ -21,6 +21,7 @@ import { keyBy } from 'lodash';
import { combineReducers } from 'redux';
import { Action as AppStateAction, Actions as AppStateActions } from '../../../store/appState';
import { ActionType } from '../../../store/utils/actions';
+import { SettingValue } from '../../../types/settings';
enum Actions {
receiveValues = 'RECEIVE_VALUES'
@@ -28,7 +29,7 @@ enum Actions {
type Action = ActionType<typeof receiveValues, Actions.receiveValues>;
-type SettingsState = T.Dict<T.SettingValue>;
+type SettingsState = T.Dict<SettingValue>;
export interface State {
components: T.Dict<SettingsState>;
@@ -74,11 +75,7 @@ function global(state: State['components'] = {}, action: Action | AppStateAction
export default combineReducers({ components, global });
-export function getValue(
- state: State,
- key: string,
- component?: string
-): T.SettingValue | undefined {
+export function getValue(state: State, key: string, component?: string): SettingValue | undefined {
if (component) {
return state.components[component] && state.components[component][key];
}
diff --git a/server/sonar-web/src/main/js/apps/settings/utils.ts b/server/sonar-web/src/main/js/apps/settings/utils.ts
index 9e4860bcf5a..0fa364c5c0f 100644
--- a/server/sonar-web/src/main/js/apps/settings/utils.ts
+++ b/server/sonar-web/src/main/js/apps/settings/utils.ts
@@ -19,6 +19,7 @@
*/
import { sanitize } from 'dompurify';
import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n';
+import { Setting, SettingCategoryDefinition, SettingDefinition } from '../../types/settings';
export const DEFAULT_CATEGORY = 'general';
@@ -32,7 +33,7 @@ export interface DefaultInputProps {
onCancel?: () => void;
onChange: (value: any) => void;
onSave?: () => void;
- setting: T.Setting;
+ setting: Setting;
value: any;
}
@@ -42,12 +43,12 @@ export function sanitizeTranslation(html: string) {
});
}
-export function getPropertyName(definition: T.SettingDefinition) {
+export function getPropertyName(definition: SettingDefinition) {
const key = `property.${definition.key}.name`;
return hasMessage(key) ? translate(key) : definition.name;
}
-export function getPropertyDescription(definition: T.SettingDefinition) {
+export function getPropertyDescription(definition: SettingDefinition) {
const key = `property.${definition.key}.description`;
return hasMessage(key) ? translate(key) : definition.description;
}
@@ -67,12 +68,12 @@ export function getSubCategoryDescription(category: string, subCategory: string)
return hasMessage(key) ? translate(key) : null;
}
-export function getUniqueName(definition: T.SettingDefinition, index?: string) {
+export function getUniqueName(definition: SettingDefinition, index?: string) {
const indexSuffix = index ? `[${index}]` : '';
return `settings[${definition.key}]${indexSuffix}`;
}
-export function getSettingValue({ definition, fieldValues, value, values }: T.Setting) {
+export function getSettingValue({ definition, fieldValues, value, values }: Setting) {
if (isCategoryDefinition(definition) && definition.multiValues) {
return values;
} else if (definition.type === 'PROPERTY_SET') {
@@ -82,7 +83,7 @@ export function getSettingValue({ definition, fieldValues, value, values }: T.Se
}
}
-export function isEmptyValue(definition: T.SettingDefinition, value: any) {
+export function isEmptyValue(definition: SettingDefinition, value: any) {
if (value == null) {
return true;
} else if (definition.type === 'BOOLEAN') {
@@ -92,13 +93,11 @@ export function isEmptyValue(definition: T.SettingDefinition, value: any) {
}
}
-export function isCategoryDefinition(
- item: T.SettingDefinition
-): item is T.SettingCategoryDefinition {
+export function isCategoryDefinition(item: SettingDefinition): item is SettingCategoryDefinition {
return Boolean((item as any).fields);
}
-export function getEmptyValue(item: T.SettingDefinition | T.SettingCategoryDefinition): any {
+export function getEmptyValue(item: SettingDefinition | SettingCategoryDefinition): any {
if (isCategoryDefinition(item)) {
if (item.multiValues) {
return [getEmptyValue({ ...item, multiValues: false })];
@@ -117,11 +116,11 @@ export function getEmptyValue(item: T.SettingDefinition | T.SettingCategoryDefin
return '';
}
-export function isDefaultOrInherited(setting: T.Setting) {
+export function isDefaultOrInherited(setting: Setting) {
return Boolean(setting.inherited);
}
-export function getDefaultValue(setting: T.Setting) {
+export function getDefaultValue(setting: Setting) {
const { definition, parentFieldValues, parentValue, parentValues } = setting;
if (definition.type === 'PASSWORD') {
diff --git a/server/sonar-web/src/main/js/types/settings.ts b/server/sonar-web/src/main/js/types/settings.ts
index 5626af2b709..c7ca6d250ab 100644
--- a/server/sonar-web/src/main/js/types/settings.ts
+++ b/server/sonar-web/src/main/js/types/settings.ts
@@ -22,3 +22,52 @@ export const enum SettingsKey {
DefaultProjectVisibility = 'projects.default.visibility',
ServerBaseUrl = 'sonar.core.serverBaseURL'
}
+
+export type Setting = SettingValue & { definition: SettingDefinition };
+
+export type SettingType =
+ | 'STRING'
+ | 'TEXT'
+ | 'JSON'
+ | 'PASSWORD'
+ | 'BOOLEAN'
+ | 'FLOAT'
+ | 'INTEGER'
+ | 'LICENSE'
+ | 'LONG'
+ | 'SINGLE_SELECT_LIST'
+ | 'PROPERTY_SET';
+
+export interface SettingDefinition {
+ description?: string;
+ key: string;
+ multiValues?: boolean;
+ name?: string;
+ options: string[];
+ type?: SettingType;
+}
+
+export interface SettingFieldDefinition extends SettingDefinition {
+ description: string;
+ name: string;
+}
+
+export interface SettingCategoryDefinition extends SettingDefinition {
+ category: string;
+ defaultValue?: string;
+ deprecatedKey?: string;
+ fields: SettingFieldDefinition[];
+ multiValues?: boolean;
+ subCategory: string;
+}
+
+export interface SettingValue {
+ fieldValues?: Array<T.Dict<string>>;
+ inherited?: boolean;
+ key: string;
+ parentFieldValues?: Array<T.Dict<string>>;
+ parentValue?: string;
+ parentValues?: string[];
+ value?: string;
+ values?: string[];
+}
diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts
index 9edfc2edf40..223967cd14e 100644
--- a/server/sonar-web/src/main/js/types/types.d.ts
+++ b/server/sonar-web/src/main/js/types/types.d.ts
@@ -672,54 +672,6 @@ declare namespace T {
export type RuleType = 'BUG' | 'VULNERABILITY' | 'CODE_SMELL' | 'SECURITY_HOTSPOT' | 'UNKNOWN';
- export type Setting = SettingValue & { definition: SettingDefinition };
-
- export type SettingType =
- | 'STRING'
- | 'TEXT'
- | 'PASSWORD'
- | 'BOOLEAN'
- | 'FLOAT'
- | 'INTEGER'
- | 'LICENSE'
- | 'LONG'
- | 'SINGLE_SELECT_LIST'
- | 'PROPERTY_SET';
-
- export interface SettingDefinition {
- description?: string;
- key: string;
- multiValues?: boolean;
- name?: string;
- options: string[];
- type?: SettingType;
- }
-
- export interface SettingFieldDefinition extends SettingDefinition {
- description: string;
- name: string;
- }
-
- export interface SettingCategoryDefinition extends SettingDefinition {
- category: string;
- defaultValue?: string;
- deprecatedKey?: string;
- fields: SettingFieldDefinition[];
- multiValues?: boolean;
- subCategory: string;
- }
-
- export interface SettingValue {
- fieldValues?: Array<T.Dict<string>>;
- inherited?: boolean;
- key: string;
- parentFieldValues?: Array<T.Dict<string>>;
- parentValue?: string;
- parentValues?: string[];
- value?: string;
- values?: string[];
- }
-
export interface Snippet {
start: number;
end: number;