* 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,
});
});
+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'],
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
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';
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;
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
/>
`;
+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}
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;
+}
ExtendedSettingDefinition,
Setting,
SettingDefinition,
+ SettingType,
SettingValue,
SettingWithCategory
} from '../../types/settings';
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;
}
--- /dev/null
+/*
+ * 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>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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} />);
+}
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;
#
#------------------------------------------------------------------------------
formatting.helplink=Formatting Help
+formatting.example.link=For a hyperlink, write: [link label](https://www.domain.com)
#------------------------------------------------------------------------------
#