]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17515 Create new setting for updating login message
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Wed, 26 Oct 2022 14:03:19 +0000 (16:03 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 27 Oct 2022 20:03:02 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/settings/__tests__/utils-test.ts
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/inputs/PrimitiveInput.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForFormattedText-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/__snapshots__/PrimitiveInput-test.tsx.snap
server/sonar-web/src/main/js/apps/settings/styles.css
server/sonar-web/src/main/js/apps/settings/utils.ts
server/sonar-web/src/main/js/components/common/FormattingTipsWithLink.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/__tests__/FormattingTipsWithLink-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/types/settings.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index f71d457f4cc22b138ee17d773a727bc9666bb395..d6e164a01f4c53cd755485dd50c9e082d6d34c71 100644 (file)
  * 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'],
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx
new file mode 100644 (file)
index 0000000..d230991
--- /dev/null
@@ -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>
+    );
+  }
+}
index fbbdac8a6824d943cc463b16581cc1b2bdd79be8..d56ca2531c2a9d1f5d8bedb8d6f7acc690aed4fd 100644 (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;
diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForFormattedText-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/__tests__/InputForFormattedText-test.tsx
new file mode 100644 (file)
index 0000000..3d3df0c
--- /dev/null
@@ -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}
+    />
+  );
+}
index f8968e76239eff3247202cd4855afa8c4c6e136c..f65e5f36800e3ee14620079a2035238355f7c167 100644 (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}
index e4caa3c42d8beba26ff863bcece7c13e4bd0bcc1..f9abd3ff9e4a1cdfaadae79e0aa91c2bf2ebba28 100644 (file)
   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;
+}
index 864459c3596a810b669dab3a4d7aaecea32a7ed4..0d0ad2160c1aad26a32c0fe6ea3e02ddb3b3c249 100644 (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;
 }
diff --git a/server/sonar-web/src/main/js/components/common/FormattingTipsWithLink.tsx b/server/sonar-web/src/main/js/components/common/FormattingTipsWithLink.tsx
new file mode 100644 (file)
index 0000000..e831cf3
--- /dev/null
@@ -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>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/FormattingTipsWithLink-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/FormattingTipsWithLink-test.tsx
new file mode 100644 (file)
index 0000000..36aa84a
--- /dev/null
@@ -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} />);
+}
index 9e9a3946ef6d921da0ad5a8a4f93a14afe684b1b..32cfbc2004ac84686031319963ec4ad8fd0739c0 100644 (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;
index 9c7bf6d7d7a99ab12bf7558310be9842f0f3bc31..54fbcf539a0afca9c778814520ff8dc2a6795a8c 100644 (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)
 
 #------------------------------------------------------------------------------
 #