@@ -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'], |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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; |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} |
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} />); | |||
} |
@@ -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; |
@@ -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) | |||
#------------------------------------------------------------------------------ | |||
# |