Browse Source

SONAR-17515 Create new setting for updating login message

tags/9.8.0.63668
Revanshu Paliwal 1 year ago
parent
commit
333225cff5

+ 45
- 2
server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts View File

@@ -18,14 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { mockComponent } from '../../../helpers/mocks/component';
import { mockDefinition } from '../../../helpers/mocks/settings';
import { mockDefinition, mockSettingValue } from '../../../helpers/mocks/settings';
import {
ExtendedSettingDefinition,
Setting,
SettingFieldDefinition,
SettingType
} from '../../../types/settings';
import { buildSettingLink, getDefaultValue, getEmptyValue } from '../utils';
import { buildSettingLink, getDefaultValue, getEmptyValue, getSettingValue } from '../utils';

const fields = [
{ key: 'foo', type: 'STRING' } as SettingFieldDefinition,
@@ -69,6 +69,49 @@ describe('#getEmptyValue()', () => {
});
});

describe('#getSettingValue()', () => {
it('should work for property sets', () => {
const setting: ExtendedSettingDefinition = {
...settingDefinition,
type: SettingType.PROPERTY_SET,
fields
};
const settingValue = mockSettingValue({ fieldValues: [{ foo: '' }] });
expect(getSettingValue(setting, settingValue)).toEqual([{ foo: '' }]);
});

it('should work for category definitions', () => {
const setting: ExtendedSettingDefinition = {
...settingDefinition,
type: SettingType.FORMATTED_TEXT,
fields,
multiValues: true
};
const settingValue = mockSettingValue({ values: ['*text*', 'text'] });
expect(getSettingValue(setting, settingValue)).toEqual(['*text*', 'text']);
});

it('should work for formatted text', () => {
const setting: ExtendedSettingDefinition = {
...settingDefinition,
type: SettingType.FORMATTED_TEXT,
fields
};
const settingValue = mockSettingValue({ values: ['*text*', 'text'] });
expect(getSettingValue(setting, settingValue)).toEqual('*text*');
});

it('should work for formatted text when values is undefined', () => {
const setting: ExtendedSettingDefinition = {
...settingDefinition,
type: SettingType.FORMATTED_TEXT,
fields
};
const settingValue = mockSettingValue({ values: undefined });
expect(getSettingValue(setting, settingValue)).toBeUndefined();
});
});

describe('#getDefaultValue()', () => {
it.each([
['true', 'settings.boolean.true'],

+ 102
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx View File

@@ -0,0 +1,102 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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';
import FormattingTipsWithLink from '../../../../components/common/FormattingTipsWithLink';
import { Button } from '../../../../components/controls/buttons';
import EditIcon from '../../../../components/icons/EditIcon';
import { translate } from '../../../../helpers/l10n';
import { sanitizeString } from '../../../../helpers/sanitize';
import { DefaultSpecializedInputProps } from '../../utils';

interface State {
editMessage: boolean;
}

export default class InputForFormattedText extends React.PureComponent<
DefaultSpecializedInputProps,
State
> {
constructor(props: DefaultSpecializedInputProps) {
super(props);
this.state = {
editMessage: !this.props.setting.hasValue
};
}

componentDidUpdate(prevProps: DefaultSpecializedInputProps) {
/*
* Reset `editMessage` if:
* - the value is reset (valueChanged -> !valueChanged)
* or
* - the value changes from outside the input (i.e. store update/reset/cancel)
*/
if (
(prevProps.hasValueChanged || this.props.setting.value !== prevProps.setting.value) &&
!this.props.hasValueChanged
) {
this.setState({ editMessage: !this.props.setting.hasValue });
}
}

handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
this.props.onChange(event.target.value);
};

toggleEditMessage = () => {
const { editMessage } = this.state;
this.setState({ editMessage: !editMessage });
};

render() {
const { editMessage } = this.state;
const { values } = this.props.setting;
// 0th value of the values array is markdown and 1st is the formatted text
const formattedValue = values ? values[1] : undefined;

return (
<div>
{editMessage ? (
<div className="display-flex-row">
<textarea
className="settings-large-input text-top spacer-right"
name={this.props.name}
onChange={this.handleInputChange}
rows={5}
value={this.props.value || ''}
/>
<FormattingTipsWithLink className="abs-width-100" />
</div>
) : (
<>
<div
className="markdown-preview markdown"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: sanitizeString(formattedValue ?? '') }}
/>
<Button className="spacer-top" onClick={this.toggleEditMessage}>
<EditIcon className="spacer-right" />
{translate('edit')}
</Button>
</>
)}
</div>
);
}
}

+ 3
- 1
server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx View File

@@ -21,6 +21,7 @@ import * as React from 'react';
import { SettingType } from '../../../../types/settings';
import { DefaultSpecializedInputProps } from '../../utils';
import InputForBoolean from './InputForBoolean';
import InputForFormattedText from './InputForFormattedText';
import InputForJSON from './InputForJSON';
import InputForNumber from './InputForNumber';
import InputForPassword from './InputForPassword';
@@ -48,7 +49,8 @@ export default function PrimitiveInput(props: DefaultSpecializedInputProps) {
INTEGER: InputForNumber,
LONG: InputForNumber,
FLOAT: InputForNumber,
SINGLE_SELECT_LIST: withOptions(definition.options)
SINGLE_SELECT_LIST: withOptions(definition.options),
FORMATTED_TEXT: InputForFormattedText
};

const InputComponent = (definition.type && typeMapping[definition.type]) || InputForString;

+ 57
- 0
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForFormattedText-test.tsx View File

@@ -0,0 +1,57 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { mockSetting } from '../../../../../helpers/mocks/settings';
import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
import { DefaultSpecializedInputProps } from '../../../utils';
import InputForFormattedText from '../InputForFormattedText';

it('renders correctly with no value for login message', () => {
renderInputForFormattedText();
expect(screen.getByRole('textbox')).toBeInTheDocument();
});

it('renders correctly with a value for login message', async () => {
const user = userEvent.setup();
renderInputForFormattedText({
setting: mockSetting({ values: ['*text*', 'text'], hasValue: true })
});
expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument();
expect(screen.getByText('text')).toBeInTheDocument();

await user.click(screen.getByRole('button', { name: 'edit' }));
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'edit' })).not.toBeInTheDocument();
});

function renderInputForFormattedText(props: Partial<DefaultSpecializedInputProps> = {}) {
renderComponent(
<InputForFormattedText
isDefault={true}
name="name"
onChange={jest.fn()}
setting={mockSetting({ value: undefined, hasValue: false })}
value="*text*"
{...props}
/>
);
}

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

@@ -58,6 +58,35 @@ exports[`should render correctly for FLOAT 1`] = `
/>
`;

exports[`should render correctly for FORMATTED_TEXT 1`] = `
<InputForFormattedText
isDefault={true}
name="name"
onChange={[MockFunction]}
setting={
Object {
"definition": Object {
"category": "foo category",
"fields": Array [],
"key": "foo",
"options": Array [],
"subCategory": "foo subCat",
"type": "FORMATTED_TEXT",
},
"hasValue": true,
"inherited": true,
"key": "foo",
"value": "42",
}
}
value={
Array [
"foo",
]
}
/>
`;

exports[`should render correctly for INTEGER 1`] = `
<InputForNumber
isDefault={true}

+ 9
- 0
server/sonar-web/src/main/js/apps/settings/styles.css View File

@@ -210,3 +210,12 @@
justify-content: space-between;
margin: 0px -16px;
}

.markdown-preview {
width: 450px;
background-color: var(--info50);
border: 1px solid var(--info200);
border-radius: 2px;
padding: 16px;
overflow-wrap: break-word;
}

+ 4
- 1
server/sonar-web/src/main/js/apps/settings/utils.ts View File

@@ -26,6 +26,7 @@ import {
ExtendedSettingDefinition,
Setting,
SettingDefinition,
SettingType,
SettingValue,
SettingWithCategory
} from '../../types/settings';
@@ -85,8 +86,10 @@ export function getSettingValue(definition: SettingDefinition, settingValue?: Se
const { fieldValues, value, values } = settingValue || {};
if (isCategoryDefinition(definition) && definition.multiValues) {
return values;
} else if (definition.type === 'PROPERTY_SET') {
} else if (definition.type === SettingType.PROPERTY_SET) {
return fieldValues;
} else if (definition.type === SettingType.FORMATTED_TEXT) {
return values ? values[0] : undefined;
}
return value;
}

+ 49
- 0
server/sonar-web/src/main/js/components/common/FormattingTipsWithLink.tsx View File

@@ -0,0 +1,49 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 classNames from 'classnames';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
import { getFormattingHelpUrl } from '../../helpers/urls';

interface Props {
className?: string;
}

export default class FormattingTipsWithLink extends React.PureComponent<Props> {
handleClick(evt: React.SyntheticEvent<HTMLAnchorElement>) {
evt.preventDefault();
window.open(
getFormattingHelpUrl(),
'Formatting',
'height=300,width=600,scrollbars=1,resizable=1'
);
}

render() {
return (
<div className={classNames('markdown-tips', this.props.className)}>
<a href="#" onClick={this.handleClick}>
{translate('formatting.helplink')}
</a>
<p className="spacer-top">{translate('formatting.example.link')}</p>
</div>
);
}
}

+ 54
- 0
server/sonar-web/src/main/js/components/common/__tests__/FormattingTipsWithLink-test.tsx View File

@@ -0,0 +1,54 @@
/*
* SonarQube
* Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import FormattingTipsWithLink from '../FormattingTipsWithLink';

const originalOpen = window.open;

beforeAll(() => {
Object.defineProperty(window, 'open', {
writable: true,
value: jest.fn()
});
});

afterAll(() => {
Object.defineProperty(window, 'open', {
writable: true,
value: originalOpen
});
});

it('should render correctly', async () => {
const user = userEvent.setup();
renderFormattingTipsWithLink();
expect(screen.getByText('formatting.helplink')).toBeInTheDocument();
expect(screen.getByText('formatting.example.link')).toBeInTheDocument();

await user.click(screen.getByRole('link'));
expect(window.open).toHaveBeenCalled();
});

function renderFormattingTipsWithLink(props: Partial<FormattingTipsWithLink['props']> = {}) {
renderComponent(<FormattingTipsWithLink {...props} />);
}

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

@@ -60,7 +60,8 @@ export enum SettingType {
LICENSE = 'LICENSE',
LONG = 'LONG',
SINGLE_SELECT_LIST = 'SINGLE_SELECT_LIST',
PROPERTY_SET = 'PROPERTY_SET'
PROPERTY_SET = 'PROPERTY_SET',
FORMATTED_TEXT = 'FORMATTED_TEXT'
}
export interface SettingDefinition {
description?: string;

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2803,6 +2803,7 @@ sonarlint-connection.unspecified-ide=an unspecified IDE
#
#------------------------------------------------------------------------------
formatting.helplink=Formatting Help
formatting.example.link=For a hyperlink, write: [link label](https://www.domain.com)

#------------------------------------------------------------------------------
#

Loading…
Cancel
Save