]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21067 Project General Settings adopt the new UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 28 Nov 2023 17:41:12 +0000 (18:41 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 1 Dec 2023 20:02:43 +0000 (20:02 +0000)
36 files changed:
server/sonar-web/design-system/src/components/Switch.tsx
server/sonar-web/design-system/src/components/input/InputField.tsx
server/sonar-web/design-system/src/components/subnavigation/SubnavigationItem.tsx
server/sonar-web/src/main/js/apps/settings/components/AllCategoriesList.tsx
server/sonar-web/src/main/js/apps/settings/components/AnalysisScope.tsx
server/sonar-web/src/main/js/apps/settings/components/DefinitionActions.tsx
server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx
server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.tsx
server/sonar-web/src/main/js/apps/settings/components/EmailForm.tsx
server/sonar-web/src/main/js/apps/settings/components/Languages.tsx
server/sonar-web/src/main/js/apps/settings/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/settings/components/SettingsAppRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/SettingsSearch.tsx
server/sonar-web/src/main/js/apps/settings/components/SettingsSearchRenderer.tsx
server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/AllCategoriesList-test.tsx
server/sonar-web/src/main/js/apps/settings/components/__tests__/SettingsApp-it.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/Input.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForBoolean.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForJSON.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForNumber.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForPassword.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSecured.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForSingleSelectList.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForString.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/InputForText.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/MultiValueInput.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/PropertySetInput.tsx
server/sonar-web/src/main/js/apps/settings/components/inputs/SimpleInput.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/AlmSpecificForm.tsx
server/sonar-web/src/main/js/apps/settings/components/pullRequestDecorationBinding/PRDecorationBindingRenderer.tsx
server/sonar-web/src/main/js/apps/settings/utils.ts
server/sonar-web/src/main/js/components/common/FormattingTipsWithLink.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7ff7b0a22d5b9db0d8909f790aef5138c7a99a88..2ec38d55a9b46a39fac56957722183db6a855786 100644 (file)
@@ -57,6 +57,7 @@ export function Switch(props: Readonly<Props>) {
       name={name}
       onClick={handleClick}
       role="switch"
+      type="button"
     >
       <CheckIconContainer active={value} disabled={disabled}>
         {value && <CheckIcon fill="currentColor" />}
index c5d97ead8ce0f15b2022195452f1fa3b22a04916..e4cb40bbbd235f177720d5b6bb6aa1e4e2c9c070 100644 (file)
@@ -129,18 +129,10 @@ const baseStyle = (props: ThemedProps) => css`
 `;
 
 const StyledInput = styled.input`
-  input[type='text']& {
-    ${getInputVariant}
-    ${baseStyle}
-    ${tw`sw-h-control`}
-  }
-
-  input[type='number']& {
-    ${getInputVariant}
-    ${baseStyle}
-    ${tw`sw-h-control`}
-  }
-  input[type='password']& {
+  input[type='text']&,
+  input[type='number']&,
+  input[type='password']&,
+  input[type='email']& {
     ${getInputVariant}
     ${baseStyle}
     ${tw`sw-h-control`}
index 54ff544fdaf241b8b31835154e2cdc9f25cb8320..94c7ee6106936cf95d99f91414c70ddb24739978 100644 (file)
@@ -19,7 +19,7 @@
  */
 import styled from '@emotion/styled';
 import classNames from 'classnames';
-import { ReactNode, useCallback } from 'react';
+import { ReactNode, SyntheticEvent, useCallback } from 'react';
 import tw, { theme as twTheme } from 'twin.macro';
 import { themeBorder, themeColor, themeContrast } from '../../helpers/theme';
 
@@ -27,20 +27,25 @@ interface Props {
   active?: boolean;
   children: ReactNode;
   className?: string;
-  innerRef?: (node: HTMLDivElement) => void;
+  innerRef?: (node: HTMLAnchorElement) => void;
   onClick: (value?: string) => void;
   value?: string;
 }
 
-export function SubnavigationItem(props: Props) {
+export function SubnavigationItem(props: Readonly<Props>) {
   const { active, className, children, innerRef, onClick, value } = props;
-  const handleClick = useCallback(() => {
-    onClick(value);
-  }, [onClick, value]);
+  const handleClick = useCallback(
+    (e: SyntheticEvent<HTMLAnchorElement>) => {
+      e.preventDefault();
+      onClick(value);
+    },
+    [onClick, value],
+  );
   return (
     <StyledSubnavigationItem
       className={classNames({ active }, className)}
       data-testid="js-subnavigation-item"
+      href="#"
       onClick={handleClick}
       ref={innerRef}
     >
@@ -49,7 +54,7 @@ export function SubnavigationItem(props: Props) {
   );
 }
 
-const StyledSubnavigationItem = styled.div`
+const StyledSubnavigationItem = styled.a`
   ${tw`sw-flex sw-items-center sw-justify-between`}
   ${tw`sw-box-border`}
   ${tw`sw-body-sm`}
@@ -60,6 +65,7 @@ const StyledSubnavigationItem = styled.div`
   padding-left: calc(${twTheme('spacing.4')} - 3px);
   color: ${themeContrast('subnavigation')};
   background-color: ${themeColor('subnavigation')};
+  border-bottom: none;
   border-left: ${themeBorder('active', 'transparent')};
   transition: 0.2 ease;
   transition-property: border-left, background-color, color;
index 9a6fb80de825826fd204fedc81c9812ca3524df3..05087d96fe68b346fbb155ea9d8b2ac3f627f923 100644 (file)
  * 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 { SubnavigationGroup, SubnavigationItem } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
-import { NavLink } from 'react-router-dom';
+import { useNavigate } from 'react-router-dom';
 import withAvailableFeatures, {
   WithAvailableFeaturesProps,
 } from '../../../app/components/available-features/withAvailableFeatures';
@@ -38,9 +38,22 @@ export interface CategoriesListProps extends WithAvailableFeaturesProps {
   selectedCategory: string;
 }
 
-function CategoriesList(props: CategoriesListProps) {
+function CategoriesList(props: Readonly<CategoriesListProps>) {
   const { categories, component, defaultCategory, selectedCategory } = props;
 
+  const navigate = useNavigate();
+
+  const openCategory = React.useCallback(
+    (category: string | undefined) => {
+      const url = component
+        ? getProjectSettingsUrl(component.key, category)
+        : getGlobalSettingsUrl(category);
+
+      navigate(url);
+    },
+    [component, navigate],
+  );
+
   const categoriesWithName = categories
     .filter((key) => !CATEGORY_OVERRIDES[key.toLowerCase()])
     .map((key) => ({
@@ -61,31 +74,20 @@ function CategoriesList(props: CategoriesListProps) {
   const sortedCategories = sortBy(categoriesWithName, (category) => category.name.toLowerCase());
 
   return (
-    <ul className="side-tabs-menu">
+    <SubnavigationGroup className="sw-box-border it__subnavigation_menu">
       {sortedCategories.map((c) => {
         const category = c.key !== defaultCategory ? c.key.toLowerCase() : undefined;
         return (
-          <li key={c.key}>
-            <NavLink
-              end
-              className={(_) =>
-                classNames({
-                  active: c.key.toLowerCase() === selectedCategory.toLowerCase(),
-                })
-              }
-              title={c.name}
-              to={
-                component
-                  ? getProjectSettingsUrl(component.key, category)
-                  : getGlobalSettingsUrl(category)
-              }
-            >
-              {c.name}
-            </NavLink>
-          </li>
+          <SubnavigationItem
+            active={c.key.toLowerCase() === selectedCategory.toLowerCase()}
+            onClick={() => openCategory(category)}
+            key={c.key}
+          >
+            {c.name}
+          </SubnavigationItem>
         );
       })}
-    </ul>
+    </SubnavigationGroup>
   );
 }
 
index fce5db8a2e34cd5a2983cc2267c31010541f4bcc..bd1747a46112b2275fd674e5a287237d28a8abc6 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import styled from '@emotion/styled';
+import { LightLabel } from 'design-system/lib';
 import * as React from 'react';
-import DocLink from '../../../components/common/DocLink';
+import DocumentationLink from '../../../components/common/DocumentationLink';
 import { translate } from '../../../helpers/l10n';
 import { AdditionalCategoryComponentProps } from './AdditionalCategories';
 import CategoryDefinitionsList from './CategoryDefinitionsList';
@@ -28,37 +30,37 @@ export function AnalysisScope(props: AdditionalCategoryComponentProps) {
 
   return (
     <>
-      <p className="spacer-bottom">
-        {translate('settings.analysis_scope.wildcards.introduction')}
-        <DocLink className="spacer-left" to="/project-administration/analysis-scope/">
-          {translate('learn_more')}
-        </DocLink>
-      </p>
+      <StyledGrid className="sw-pt-6 sw-px-6 sw-gap-2">
+        <p className="sw-col-span-2">
+          {translate('settings.analysis_scope.wildcards.introduction')}
+        </p>
 
-      <table className="data spacer-bottom">
-        <tbody>
-          <tr>
-            <td>*</td>
-            <td>{translate('settings.analysis_scope.wildcards.zero_more_char')}</td>
-          </tr>
-          <tr>
-            <td>**</td>
-            <td>{translate('settings.analysis_scope.wildcards.zero_more_dir')}</td>
-          </tr>
-          <tr>
-            <td>?</td>
-            <td>{translate('settings.analysis_scope.wildcards.single_char')}</td>
-          </tr>
-        </tbody>
-      </table>
+        <span>*</span>
+        <LightLabel>{translate('settings.analysis_scope.wildcards.zero_more_char')}</LightLabel>
 
-      <div className="settings-sub-category">
-        <CategoryDefinitionsList
-          category={selectedCategory}
-          component={component}
-          definitions={definitions}
-        />
-      </div>
+        <span>**</span>
+        <LightLabel>{translate('settings.analysis_scope.wildcards.zero_more_dir')}</LightLabel>
+
+        <span>?</span>
+        <LightLabel>{translate('settings.analysis_scope.wildcards.single_char')}</LightLabel>
+
+        <div className="sw-col-span-2">
+          <DocumentationLink to="/project-administration/analysis-scope/">
+            {translate('learn_more')}
+          </DocumentationLink>
+        </div>
+      </StyledGrid>
+
+      <CategoryDefinitionsList
+        category={selectedCategory}
+        component={component}
+        definitions={definitions}
+      />
     </>
   );
 }
+
+const StyledGrid = styled.div`
+  display: grid;
+  grid-template-columns: 1.5rem auto;
+`;
index a72e37a629c08064584b733870dd087ac65a4e02..7745de1d70a5c7463f35fc38b4503b807fb93e7d 100644 (file)
@@ -17,9 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ButtonPrimary, ButtonSecondary, DangerButtonPrimary, Modal, Note } from 'design-system';
 import * as React from 'react';
-import { Button, ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import Modal from '../../../components/controls/Modal';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Setting } from '../../../types/settings';
 import { getDefaultValue, getPropertyName, isEmptyValue } from '../utils';
@@ -38,6 +37,8 @@ type Props = {
 
 type State = { reseting: boolean };
 
+const MODAL_FORM_ID = 'SETTINGS.RESET_CONFIRM.FORM';
+
 export default class DefinitionActions extends React.PureComponent<Props, State> {
   state: State = { reseting: false };
 
@@ -49,7 +50,8 @@ export default class DefinitionActions extends React.PureComponent<Props, State>
     this.setState({ reseting: true });
   };
 
-  handleSubmit = () => {
+  handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+    e.preventDefault();
     this.props.onReset();
     this.handleClose();
   };
@@ -57,78 +59,68 @@ export default class DefinitionActions extends React.PureComponent<Props, State>
   renderModal() {
     const header = translate('settings.reset_confirm.title');
     return (
-      <Modal contentLabel={header} onRequestClose={this.handleClose}>
-        <header className="modal-head">
-          <h2>{header}</h2>
-        </header>
-        <form onSubmit={this.handleSubmit}>
-          <div className="modal-body">
+      <Modal
+        headerTitle={header}
+        onClose={this.handleClose}
+        body={
+          <form id={MODAL_FORM_ID} onSubmit={this.handleSubmit}>
             <p>{translate('settings.reset_confirm.description')}</p>
-          </div>
-          <footer className="modal-foot">
-            <SubmitButton className="button-red">{translate('reset_verb')}</SubmitButton>
-            <ResetButtonLink onClick={this.handleClose}>{translate('cancel')}</ResetButtonLink>
-          </footer>
-        </form>
-      </Modal>
+          </form>
+        }
+        primaryButton={
+          <DangerButtonPrimary type="submit" form={MODAL_FORM_ID}>
+            {translate('reset_verb')}
+          </DangerButtonPrimary>
+        }
+        secondaryButtonLabel={translate('cancel')}
+      />
     );
   }
 
   render() {
-    const { setting, changedValue, isDefault, isEditing, hasValueChanged } = this.props;
+    const { setting, changedValue, isDefault, isEditing, hasValueChanged, hasError } = this.props;
     const hasBeenChangedToEmptyValue =
       changedValue !== undefined && isEmptyValue(setting.definition, changedValue);
     const showReset = hasBeenChangedToEmptyValue || (!isDefault && setting.hasValue);
     const showCancel = hasValueChanged || isEditing;
 
     return (
-      <>
-        {isDefault && !hasValueChanged && (
-          <div className="spacer-top note" style={{ lineHeight: '24px' }}>
-            {translate('settings._default')}
-          </div>
+      <div className="sw-mt-8">
+        {hasValueChanged && (
+          <ButtonPrimary className="sw-mr-3" disabled={hasError} onClick={this.props.onSave}>
+            {translate('save')}
+          </ButtonPrimary>
         )}
-        <div className="settings-definition-changes nowrap">
-          {hasValueChanged && (
-            <Button
-              className="spacer-right button-success"
-              disabled={this.props.hasError}
-              onClick={this.props.onSave}
-            >
-              {translate('save')}
-            </Button>
-          )}
 
-          {showReset && (
-            <Button
-              className="spacer-right"
-              aria-label={translateWithParameters(
-                'settings.definition.reset',
-                getPropertyName(setting.definition),
-              )}
-              onClick={this.handleReset}
-            >
-              {translate('reset_verb')}
-            </Button>
-          )}
+        {showReset && (
+          <ButtonSecondary
+            className="sw-mr-3"
+            aria-label={translateWithParameters(
+              'settings.definition.reset',
+              getPropertyName(setting.definition),
+            )}
+            onClick={this.handleReset}
+          >
+            {translate('reset_verb')}
+          </ButtonSecondary>
+        )}
 
-          {showCancel && (
-            <ResetButtonLink className="spacer-right" onClick={this.props.onCancel}>
-              {translate('cancel')}
-            </ResetButtonLink>
-          )}
+        {showCancel && (
+          <ButtonSecondary className="sw-mr-3" onClick={this.props.onCancel}>
+            {translate('cancel')}
+          </ButtonSecondary>
+        )}
 
-          {showReset && (
-            <span className="note">
-              {translate('default')}
-              {': '}
-              {getDefaultValue(setting)}
-            </span>
-          )}
+        {showReset && (
+          <Note>
+            {translate('default')}
+            {': '}
+            {getDefaultValue(setting)}
+          </Note>
+        )}
 
-          {this.state.reseting && this.renderModal()}
-        </div>
-      </>
+        {this.state.reseting && this.renderModal()}
+      </div>
     );
   }
 }
index 6f866b2a9e3bb7d755982154200f1819766dda7c..e85bf25586bbf1cbb3ec3e8a5099bb3685d5e964 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Note, SubHeading } from 'design-system';
 import * as React from 'react';
 import Tooltip from '../../../components/controls/Tooltip';
 import { translateWithParameters } from '../../../helpers/l10n';
@@ -28,28 +29,26 @@ interface Props {
   definition: ExtendedSettingDefinition;
 }
 
-export default function DefinitionDescription({ definition }: Props) {
+export default function DefinitionDescription({ definition }: Readonly<Props>) {
   const propertyName = getPropertyName(definition);
   const description = getPropertyDescription(definition);
 
   return (
-    <div className="settings-definition-left">
-      <h4 className="settings-definition-name" title={propertyName}>
-        {propertyName}
-      </h4>
+    <div className="sw-w-abs-300">
+      <SubHeading title={propertyName}>{propertyName}</SubHeading>
 
       {description && (
         <div
-          className="markdown small spacer-top"
+          className="markdown sw-mt-1"
           // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{ __html: sanitizeStringRestricted(description) }}
         />
       )}
 
       <Tooltip overlay={translateWithParameters('settings.key_x', definition.key)}>
-        <div className="settings-definition-key note little-spacer-top">
+        <Note as="div" className="sw-mt-4">
           {translateWithParameters('settings.key_x', definition.key)}
-        </div>
+        </Note>
       </Tooltip>
     </div>
   );
index 47249b579bebcd4e7ecf818abdce470644e076fc..9d0dab638f1593603a8684e1d0a7ec6cf2135156 100644 (file)
  * 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 { FlagMessage, Note, Spinner, TextError } from 'design-system';
 import * as React from 'react';
-import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
-import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { ExtendedSettingDefinition, SettingValue } from '../../../types/settings';
 import { combineDefinitionAndSettingValue, getSettingValue, isDefaultOrInherited } from '../utils';
@@ -45,7 +43,7 @@ export interface DefinitionRendererProps {
 
 const formNoop = (e: React.FormEvent<HTMLFormElement>) => e.preventDefault();
 
-export default function DefinitionRenderer(props: DefinitionRendererProps) {
+export default function DefinitionRenderer(props: Readonly<DefinitionRendererProps>) {
   const { changedValue, loading, validationMessage, settingValue, success, definition, isEditing } =
     props;
 
@@ -57,39 +55,10 @@ export default function DefinitionRenderer(props: DefinitionRendererProps) {
   const settingDefinitionAndValue = combineDefinitionAndSettingValue(definition, settingValue);
 
   return (
-    <div
-      className={classNames('settings-definition', {
-        'settings-definition-changed': hasValueChanged,
-      })}
-      data-key={definition.key}
-    >
+    <div data-key={definition.key} className="sw-flex sw-gap-12">
       <DefinitionDescription definition={definition} />
 
-      <div className="settings-definition-right">
-        <div className="settings-definition-state">
-          {loading && (
-            <span className="text-info">
-              <i className="spinner spacer-right" />
-              {translate('settings.state.saving')}
-            </span>
-          )}
-
-          {!loading && validationMessage && (
-            <span className="text-danger">
-              <AlertErrorIcon className="spacer-right" />
-              <span>
-                {translateWithParameters('settings.state.validation_failed', validationMessage)}
-              </span>
-            </span>
-          )}
-
-          {!loading && !hasError && success && (
-            <span className="text-success">
-              <AlertSuccessIcon className="spacer-right" />
-              {translate('settings.state.saved')}
-            </span>
-          )}
-        </div>
+      <div className="sw-flex-1">
         <form onSubmit={formNoop}>
           <Input
             hasValueChanged={hasValueChanged}
@@ -98,9 +67,35 @@ export default function DefinitionRenderer(props: DefinitionRendererProps) {
             onSave={props.onSave}
             onEditing={props.onEditing}
             isEditing={isEditing}
+            isInvalid={hasError}
             setting={settingDefinitionAndValue}
             value={effectiveValue}
           />
+
+          <div className="sw-mt-2">
+            {loading && (
+              <div className="sw-flex">
+                <Spinner />
+                <Note className="sw-ml-2">{translate('settings.state.saving')}</Note>
+              </div>
+            )}
+
+            {!loading && validationMessage && (
+              <div>
+                <TextError
+                  text={translateWithParameters(
+                    'settings.state.validation_failed',
+                    validationMessage,
+                  )}
+                />
+              </div>
+            )}
+
+            {!loading && !hasError && success && (
+              <FlagMessage variant="success">{translate('settings.state.saved')}</FlagMessage>
+            )}
+          </div>
+
           <DefinitionActions
             changedValue={changedValue}
             hasError={hasError}
index 45c04a0978a9d50425ff5c239a1882e2be78b7ed..1cb611ea129cff314f5e1ad9c3c54248c7b8d1c2 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import styled from '@emotion/styled';
+import { themeBorder } from 'design-system/lib';
 import * as React from 'react';
 import { SettingDefinitionAndValue } from '../../../types/settings';
 import { Component } from '../../../types/types';
@@ -28,14 +30,15 @@ interface Props {
   settings: SettingDefinitionAndValue[];
 }
 
-export default function DefinitionsList(props: Props) {
+export default function DefinitionsList(props: Readonly<Props>) {
   const { component, settings } = props;
   return (
-    <ul className="settings-definitions-list">
+    <ul>
       {settings.map((setting) => (
-        <li
+        <StyledListItem
+          className="sw-p-6"
           key={setting.definition.key}
-          data-key={setting.definition.key}
+          data-scroll-key={setting.definition.key}
           ref={props.scrollToDefinition}
         >
           <Definition
@@ -43,8 +46,14 @@ export default function DefinitionsList(props: Props) {
             definition={setting.definition}
             initialSettingValue={setting.settingValue}
           />
-        </li>
+        </StyledListItem>
       ))}
     </ul>
   );
 }
+
+const StyledListItem = styled.li`
+  & + & {
+    border-top: ${themeBorder('default')};
+  }
+`;
index 47dd8d91b1a40b40f91a097a095bd40a26d97625..ab0e8cf45abc8167e15a8c923a0d5e933298c450 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import {
+  BasicSeparator,
+  ButtonPrimary,
+  FlagMessage,
+  FormField,
+  InputField,
+  InputTextArea,
+  Spinner,
+  SubHeading,
+} from 'design-system';
 import * as React from 'react';
 import { sendTestEmail } from '../../../api/settings';
 import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
-import { SubmitButton } from '../../../components/controls/buttons';
-import { Alert } from '../../../components/ui/Alert';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
-import Spinner from '../../../components/ui/Spinner';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { parseError } from '../../../helpers/request';
 import { LoggedInUser } from '../../../types/users';
@@ -97,80 +103,65 @@ export class EmailForm extends React.PureComponent<Props, State> {
   render() {
     const { error, loading, message, recipient, subject, success } = this.state;
     return (
-      <div className="settings-definition">
-        <div className="settings-definition-left">
-          <h3 className="settings-definition-name">
-            {translate('email_configuration.test.title')}
-          </h3>
-        </div>
-
-        <form className="settings-definition-right" onSubmit={this.handleFormSubmit}>
-          {success && (
-            <div className="form-field">
-              <Alert variant="success">
-                {translateWithParameters('email_configuration.test.email_was_sent_to_x', success)}
-              </Alert>
-            </div>
-          )}
-
-          {error !== undefined && (
-            <div className="form-field">
-              <Alert variant="error">{error}</Alert>
+      <>
+        <BasicSeparator />
+        <div className="sw-p-6 sw-flex sw-gap-12">
+          <div className="sw-w-abs-300">
+            <SubHeading>{translate('email_configuration.test.title')}</SubHeading>
+            <div className="sw-mt-1">
+              <MandatoryFieldsExplanation />
             </div>
-          )}
-
-          <MandatoryFieldsExplanation className="form-field" />
-
-          <div className="form-field">
-            <label htmlFor="test-email-to">
-              {translate('email_configuration.test.to_address')}
-              <MandatoryFieldMarker />
-            </label>
-            <input
-              className="settings-large-input"
-              disabled={loading}
-              id="test-email-to"
-              onChange={this.onRecipientChange}
-              required
-              type="email"
-              value={recipient}
-            />
-          </div>
-          <div className="form-field">
-            <label htmlFor="test-email-subject">
-              {translate('email_configuration.test.subject')}
-            </label>
-            <input
-              className="settings-large-input"
-              disabled={loading}
-              id="test-email-subject"
-              onChange={this.onSubjectChange}
-              type="text"
-              value={subject}
-            />
-          </div>
-          <div className="form-field">
-            <label htmlFor="test-email-message">
-              {translate('email_configuration.test.message')}
-              <MandatoryFieldMarker />
-            </label>
-            <textarea
-              className="settings-large-input"
-              disabled={loading}
-              id="test-email-message"
-              onChange={this.onMessageChange}
-              required
-              rows={5}
-              value={message}
-            />
           </div>
 
-          <SubmitButton disabled={loading}>
-            {translate('email_configuration.test.send')}
-          </SubmitButton>
-          {loading && <Spinner className="spacer-left" />}
-        </form>
-      </div>
+          <form className="sw-flex-1" onSubmit={this.handleFormSubmit}>
+            {success && (
+              <FlagMessage variant="success">
+                {translateWithParameters('email_configuration.test.email_was_sent_to_x', success)}
+              </FlagMessage>
+            )}
+
+            {error !== undefined && <FlagMessage variant="error">{error}</FlagMessage>}
+
+            <FormField label={translate('email_configuration.test.to_address')} required>
+              <InputField
+                disabled={loading}
+                id="test-email-to"
+                onChange={this.onRecipientChange}
+                required
+                size="large"
+                type="email"
+                value={recipient}
+              />
+            </FormField>
+            <FormField label={translate('email_configuration.test.subject')}>
+              <InputField
+                disabled={loading}
+                id="test-email-subject"
+                onChange={this.onSubjectChange}
+                size="large"
+                type="text"
+                value={subject}
+              />
+            </FormField>
+            <FormField label={translate('email_configuration.test.message')} required>
+              <InputTextArea
+                disabled={loading}
+                id="test-email-message"
+                onChange={this.onMessageChange}
+                required
+                rows={5}
+                size="large"
+                value={message}
+              />
+            </FormField>
+
+            <ButtonPrimary disabled={loading} type="submit" className="sw-mt-2">
+              {translate('email_configuration.test.send')}
+            </ButtonPrimary>
+            <Spinner loading={loading} className="sw-ml-2" />
+          </form>
+        </div>
+      </>
     );
   }
 }
index bb3eeae9f4932c3eef2398d8dc9ebdbb7902e461..f9930bc0eb9ff1b47f5a77fe3dd6bdc203574801 100644 (file)
@@ -17,8 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { PopupZLevel, SearchSelectDropdown, SubHeading } from 'design-system';
 import * as React from 'react';
-import Select from '../../../components/controls/Select';
+import { Options } from 'react-select';
 import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
 import { translate } from '../../../helpers/l10n';
 import { CATEGORY_OVERRIDES, LANGUAGES_CATEGORY } from '../constants';
@@ -37,7 +38,7 @@ interface SelectOption {
   value: string;
 }
 
-export function Languages(props: LanguagesProps) {
+export function Languages(props: Readonly<LanguagesProps>) {
   const { categories, component, definitions, location, router, selectedCategory } = props;
   const { availableLanguages, selectedLanguage } = getLanguages(categories, selectedCategory);
 
@@ -48,29 +49,45 @@ export function Languages(props: LanguagesProps) {
     });
   };
 
+  const handleLanguagesSearch = React.useCallback(
+    (query: string, cb: (options: Options<SelectOption>) => void) => {
+      const normalizedQuery = query.toLowerCase();
+
+      cb(
+        availableLanguages.filter(
+          (lang) =>
+            lang.label.toLowerCase().includes(normalizedQuery) ||
+            lang.value.includes(normalizedQuery),
+        ),
+      );
+    },
+    [availableLanguages],
+  );
+
   return (
     <>
-      <h2 id="languages-category-title" className="settings-sub-category-name">
+      <SubHeading id="languages-category-title">
         {translate('property.category.languages')}
-      </h2>
+      </SubHeading>
       <div data-test="language-select">
-        <Select
-          aria-labelledby="languages-category-title"
+        <SearchSelectDropdown
+          defaultOptions={availableLanguages}
+          controlAriaLabel={translate('property.category.languages')}
           className="input-large select-settings-language"
           onChange={handleOnChange}
-          options={availableLanguages}
+          loadOptions={handleLanguagesSearch}
           placeholder={translate('settings.languages.select_a_language_placeholder')}
+          size="large"
+          zLevel={PopupZLevel.Content}
           value={availableLanguages.find((language) => language.value === selectedLanguage)}
         />
       </div>
       {selectedLanguage && (
-        <div className="settings-sub-category">
-          <CategoryDefinitionsList
-            category={selectedLanguage}
-            component={component}
-            definitions={definitions}
-          />
-        </div>
+        <CategoryDefinitionsList
+          category={selectedLanguage}
+          component={component}
+          definitions={definitions}
+        />
       )}
     </>
   );
index 7d42d29c7add446aeb3ef7ccfe8f90477a3ad9de..593285261c26d2a9ba620501b68b6922b96ad5a6 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Title } from 'design-system';
 import * as React from 'react';
 import InstanceMessage from '../../../components/common/InstanceMessage';
 import { translate } from '../../../helpers/l10n';
@@ -29,7 +30,7 @@ export interface PageHeaderProps {
   definitions: ExtendedSettingDefinition[];
 }
 
-export default function PageHeader({ component, definitions }: PageHeaderProps) {
+export default function PageHeader({ component, definitions }: Readonly<PageHeaderProps>) {
   const title = component ? translate('project_settings.page') : translate('settings.page');
 
   const description = component ? (
@@ -39,18 +40,10 @@ export default function PageHeader({ component, definitions }: PageHeaderProps)
   );
 
   return (
-    <header className="top-bar-outer">
-      <div className="top-bar">
-        <div className="top-bar-inner bordered-bottom big-padded-top padded-bottom">
-          <h2 className="page-title">{title}</h2>
-          <div className="page-description spacer-top">{description}</div>
-          <SettingsSearch
-            className="big-spacer-top"
-            component={component}
-            definitions={definitions}
-          />
-        </div>
-      </div>
+    <header className="sw-mb-5">
+      <Title className="sw-mb-4">{title}</Title>
+      <p className="sw-mb-4">{description}</p>
+      <SettingsSearch component={component} definitions={definitions} />
     </header>
   );
 }
index f36e699a10650eba33e4a2e2b116a21ff1193bca..5b7b10c8d229d1dd8230b5918076f47f355636f5 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import styled from '@emotion/styled';
+import { LargeCenteredLayout, PageContentFontWrapper, themeBorder } from 'design-system';
 import { uniqBy } from 'lodash';
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
-import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
 import Suggestions from '../../../components/embed-docs-modal/Suggestions';
 import { Location, withRouter } from '../../../components/hoc/withRouter';
 import { translate } from '../../../helpers/l10n';
@@ -40,7 +41,7 @@ export interface SettingsAppRendererProps {
   location: Location;
 }
 
-function SettingsAppRenderer(props: SettingsAppRendererProps) {
+function SettingsAppRenderer(props: Readonly<SettingsAppRendererProps>) {
   const { definitions, component, loading, location } = props;
 
   const categories = React.useMemo(() => {
@@ -67,51 +68,52 @@ function SettingsAppRenderer(props: SettingsAppRendererProps) {
       (!isProjectSettings && foundAdditionalCategory.availableGlobally));
 
   return (
-    <main id="settings-page">
+    <LargeCenteredLayout id="settings-page">
       <Suggestions suggestions="settings" />
       <Helmet defer={false} title={translate('settings.page')} />
-      <PageHeader component={component} definitions={definitions} />
 
-      <div className="layout-page">
-        <ScreenPositionHelper className="layout-page-side-outer">
-          {({ top }) => (
-            <div className="layout-page-side" style={{ top }}>
-              <div className="layout-page-side-inner">
-                <AllCategoriesList
-                  categories={categories}
-                  component={component}
-                  defaultCategory={defaultCategory}
-                  selectedCategory={selectedCategory}
-                />
-              </div>
-            </div>
-          )}
-        </ScreenPositionHelper>
+      <PageContentFontWrapper className="sw-my-8">
+        <PageHeader component={component} definitions={definitions} />
 
-        <div className="layout-page-main">
-          <div className="layout-page-main-inner">
-            {/* Adding a key to force re-rendering of the category content, so that it resets the scroll position */}
-            <div className="big-padded" key={selectedCategory}>
-              {shouldRenderAdditionalCategory ? (
-                foundAdditionalCategory.renderComponent({
-                  categories,
-                  component,
-                  definitions,
-                  selectedCategory: originalCategory,
-                })
-              ) : (
-                <CategoryDefinitionsList
-                  category={selectedCategory}
-                  component={component}
-                  definitions={definitions}
-                />
-              )}
-            </div>
+        <div className="sw-body-sm sw-flex sw-items-stretch sw-justify-between">
+          <div className="sw-min-w-abs-250">
+            <AllCategoriesList
+              categories={categories}
+              component={component}
+              defaultCategory={defaultCategory}
+              selectedCategory={selectedCategory}
+            />
           </div>
+
+          {/* Adding a key to force re-rendering of the category content, so that it resets the scroll position */}
+          <StyledBox
+            className="it__settings_list sw-flex-1 sw-p-6 sw-min-w-0"
+            key={selectedCategory}
+          >
+            {shouldRenderAdditionalCategory ? (
+              foundAdditionalCategory.renderComponent({
+                categories,
+                component,
+                definitions,
+                selectedCategory: originalCategory,
+              })
+            ) : (
+              <CategoryDefinitionsList
+                category={selectedCategory}
+                component={component}
+                definitions={definitions}
+              />
+            )}
+          </StyledBox>
         </div>
-      </div>
-    </main>
+      </PageContentFontWrapper>
+    </LargeCenteredLayout>
   );
 }
 
 export default withRouter(SettingsAppRenderer);
+
+const StyledBox = styled.div`
+  border: ${themeBorder('default', 'subnavigationBorder')};
+  margin-left: -1px;
+`;
index 6ba890aef1c51911c92d26f93ce46ab04fb0cec2..de78376ac261071f343d9aacb87ffab1e88e982b 100644 (file)
@@ -32,7 +32,6 @@ import {
 import SettingsSearchRenderer from './SettingsSearchRenderer';
 
 interface Props {
-  className?: string;
   component?: Component;
   definitions: ExtendedSettingDefinition[];
   router: Router;
@@ -176,11 +175,10 @@ export class SettingsSearch extends React.Component<Props, State> {
   };
 
   render() {
-    const { className, component } = this.props;
+    const { component } = this.props;
 
     return (
       <SettingsSearchRenderer
-        className={className}
         component={component}
         onClickOutside={this.hideResults}
         onMouseOverResult={this.handleMouseOverResult}
index 872eb1ff07119efe708095ced71c486469aaf605..12b53e68482abe7b99af336192cdd42dcf5094c1 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import styled from '@emotion/styled';
 import classNames from 'classnames';
+import {
+  DropdownMenu,
+  InputSearch,
+  LinkBox,
+  Note,
+  OutsideClickHandler,
+  Popup,
+  PopupPlacement,
+  themeColor,
+} from 'design-system';
 import * as React from 'react';
-import Link from '../../../components/common/Link';
-import { DropdownOverlay } from '../../../components/controls/Dropdown';
-import OutsideClickHandler from '../../../components/controls/OutsideClickHandler';
-import SearchBox from '../../../components/controls/SearchBox';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { ExtendedSettingDefinition } from '../../../types/settings';
 import { Component } from '../../../types/types';
 import { buildSettingLink, isRealSettingKey } from '../utils';
 
+const SEARCH_INPUT_ID = 'settings-search-input';
 export interface SettingsSearchRendererProps {
-  className?: string;
   component?: Component;
   results?: ExtendedSettingDefinition[];
   searchQuery: string;
@@ -42,63 +49,108 @@ export interface SettingsSearchRendererProps {
   onSearchInputKeyDown: (event: React.KeyboardEvent) => void;
 }
 
-export default function SettingsSearchRenderer(props: SettingsSearchRendererProps) {
-  const { className, component, results, searchQuery, selectedResult, showResults } = props;
+export default function SettingsSearchRenderer(props: Readonly<SettingsSearchRendererProps>) {
+  const { component, results, searchQuery, selectedResult, showResults } = props;
 
   const selectedNodeRef = React.useRef<HTMLLIElement>(null);
 
   React.useEffect(() => {
-    selectedNodeRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
+    selectedNodeRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
   });
 
   return (
     <OutsideClickHandler onClickOutside={props.onClickOutside}>
-      <div className={classNames('dropdown', className)}>
-        <SearchBox
-          onChange={props.onSearchInputChange}
-          onFocus={props.onSearchInputFocus}
-          onKeyDown={props.onSearchInputKeyDown}
-          placeholder={translate('settings.search.placeholder')}
-          value={searchQuery}
-        />
-        {showResults && (
-          <DropdownOverlay noPadding>
-            <ul
-              className="settings-search-results menu"
-              title={translate('settings.search.results')}
+      <Popup
+        allowResizing
+        placement={PopupPlacement.BottomLeft}
+        overlay={
+          showResults && (
+            <DropdownMenu
+              className="sw-overflow-y-auto sw-overflow-x-hidden"
+              maxHeight="50vh"
+              size="auto"
+              aria-owns={SEARCH_INPUT_ID}
             >
               {results && results.length > 0 ? (
                 results.map((r) => (
-                  <li
+                  <ResultItem
                     key={r.key}
-                    className={classNames('spacer-bottom spacer-top', {
-                      active: selectedResult === r.key,
-                    })}
-                    ref={selectedResult === r.key ? selectedNodeRef : undefined}
+                    active={selectedResult === r.key}
+                    resultKey={r.key}
+                    onMouseEnter={props.onMouseOverResult}
+                    innerRef={selectedResult === r.key ? selectedNodeRef : undefined}
                   >
-                    <Link
+                    <LinkBox
+                      className="sw-block sw-py-2 sw-px-4"
                       onClick={props.onClickOutside}
-                      onMouseEnter={() => props.onMouseOverResult(r.key)}
                       to={buildSettingLink(r, component)}
                     >
-                      <div className="settings-search-result-title display-flex-space-between">
-                        <h3>{r.name || r.subCategory}</h3>
-                      </div>
+                      <h3 className="sw-body-sm-highlight">{r.name ?? r.subCategory}</h3>
                       {isRealSettingKey(r.key) && (
-                        <div className="note spacer-top">
-                          {translateWithParameters('settings.key_x', r.key)}
-                        </div>
+                        <StyledNote>{translateWithParameters('settings.key_x', r.key)}</StyledNote>
                       )}
-                    </Link>
-                  </li>
+                    </LinkBox>
+                  </ResultItem>
                 ))
               ) : (
-                <div className="big-padded">{translate('no_results')}</div>
+                <div className="sw-p-4">{translate('no_results')}</div>
               )}
-            </ul>
-          </DropdownOverlay>
-        )}
-      </div>
+            </DropdownMenu>
+          )
+        }
+      >
+        <InputSearch
+          id={SEARCH_INPUT_ID}
+          onChange={props.onSearchInputChange}
+          onFocus={props.onSearchInputFocus}
+          onKeyDown={props.onSearchInputKeyDown}
+          placeholder={translate('settings.search.placeholder')}
+          value={searchQuery}
+        />
+      </Popup>
     </OutsideClickHandler>
   );
 }
+
+interface ResultItemProps {
+  active: boolean;
+  children: React.ReactNode;
+  innerRef?: React.Ref<HTMLLIElement>;
+  onMouseEnter: (resultKey: string) => void;
+  resultKey: string;
+}
+
+function ResultItem({
+  active,
+  onMouseEnter,
+  children,
+  resultKey,
+  innerRef,
+}: Readonly<ResultItemProps>) {
+  const handleMouseEnter = React.useCallback(() => {
+    onMouseEnter(resultKey);
+  }, [onMouseEnter, resultKey]);
+
+  return (
+    <StyledItem className={classNames({ active })} onMouseEnter={handleMouseEnter} ref={innerRef}>
+      {children}
+    </StyledItem>
+  );
+}
+
+const StyledItem = styled.li`
+  &:focus,
+  &.active {
+    background-color: ${themeColor('dropdownMenuHover')};
+
+    h3 {
+      color: ${themeColor('linkActive')};
+    }
+  }
+`;
+
+const StyledNote = styled(Note)`
+  .active & {
+    text-decoration: underline;
+  }
+`;
index 82322e784bceacdd142a13f01a6889ec87f2d128..14ea38352d18a6d06ed47ca2e74792d9e2856100 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { BasicSeparator, Note, SubTitle } from 'design-system';
 import { groupBy, sortBy } from 'lodash';
 import * as React from 'react';
 import { Location, withRouter } from '../../../components/hoc/withRouter';
@@ -40,7 +41,7 @@ class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryDefiniti
   componentDidUpdate(prevProps: SubCategoryDefinitionsListProps) {
     const { hash } = this.props.location;
     if (hash && prevProps.location.hash !== hash) {
-      const query = `[data-key=${hash.substring(1).replace(/[.#/]/g, '\\$&')}]`;
+      const query = `[data-scroll-key=${hash.substring(1).replace(/[.#/]/g, '\\$&')}]`;
       const element = document.querySelector<HTMLHeadingElement | HTMLLIElement>(query);
       this.scrollToSubCategoryOrDefinition(element);
     }
@@ -49,7 +50,7 @@ class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryDefiniti
   scrollToSubCategoryOrDefinition = (element: HTMLHeadingElement | HTMLLIElement | null) => {
     if (element) {
       const { hash } = this.props.location;
-      if (hash && hash.substring(1) === element.getAttribute('data-key')) {
+      if (hash && hash.substring(1) === element.getAttribute('data-scroll-key')) {
         element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
       }
     }
@@ -78,33 +79,39 @@ class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryDefiniti
       ? sortedSubCategories.filter((c) => c.key === subCategory)
       : sortedSubCategories;
     return (
-      <ul className="settings-sub-categories-list">
-        {filteredSubCategories.map((subCategory) => (
-          <li key={subCategory.key}>
+      <ul>
+        {filteredSubCategories.map((subCategory, index) => (
+          <li className="sw-p-6" key={subCategory.key}>
             {displaySubCategoryTitle && (
-              <h3
-                className="settings-sub-category-name h2"
+              <SubTitle
+                as="h3"
                 data-key={subCategory.key}
                 ref={this.scrollToSubCategoryOrDefinition}
               >
                 {subCategory.name}
-              </h3>
+              </SubTitle>
             )}
             {subCategory.description != null && (
-              <div
-                className="settings-sub-category-description markdown"
+              <Note
+                className="markdown"
                 // eslint-disable-next-line react/no-danger
                 dangerouslySetInnerHTML={{
                   __html: sanitizeStringRestricted(subCategory.description),
                 }}
               />
             )}
+            <BasicSeparator className="sw-mt-6" />
             <DefinitionsList
               component={component}
               scrollToDefinition={this.scrollToSubCategoryOrDefinition}
               settings={bySubCategory[subCategory.key]}
             />
             {this.renderEmailForm(subCategory.key)}
+
+            {
+              // Add a separator to all but the last element
+              index !== filteredSubCategories.length - 1 && <BasicSeparator />
+            }
           </li>
         ))}
       </ul>
index dcd217cb50bc52193841beeec9d33b1c7e7ac6c3..9682f730c989e3c3ec245d972b8d7b2fe6857707 100644 (file)
@@ -69,7 +69,7 @@ it('should render correctly', () => {
   expect(screen.getByText('CAT_2_NAME')).toBeInTheDocument();
   expect(screen.queryByText('CAT_3_NAME')).not.toBeInTheDocument();
   expect(screen.queryByText('CAT_4_NAME')).not.toBeInTheDocument();
-  expect(screen.getByText('CAT_2_NAME')).toHaveClass('active', { exact: true });
+  expect(screen.getByText('CAT_2_NAME')).toHaveClass('active');
 });
 
 it('should correctly for project', () => {
index f842bb820ff8b1aa6d21099a64f2a98acc33cce9..7a7e9b032b039ca95606b21fe9fcc606d34bc520 100644 (file)
@@ -56,7 +56,7 @@ const ui = {
   jsGeneralSubCategoryHeading: byRole('heading', { name: 'property.category.javascript.General' }),
 
   settingsSearchInput: byRole('searchbox', { name: 'settings.search.placeholder' }),
-  searchList: byRole('list', { name: 'settings.search.results' }),
+  searchResultsList: byRole('menu'),
   searchItem: (key: string) => byRole('link', { name: new RegExp(key) }),
   searchClear: byRole('button', { name: 'clear' }),
 
@@ -118,12 +118,12 @@ describe('Global Settings', () => {
 
     // List popup should be closed if input is empty
     await user.click(ui.settingsSearchInput.get());
-    expect(ui.searchList.query()).not.toBeInTheDocument();
+    expect(ui.searchResultsList.query()).not.toBeInTheDocument();
 
     // Should shot 'no results' based on input value
     await user.type(ui.settingsSearchInput.get(), 'asdjasnd');
-    expect(ui.searchList.get()).toBeInTheDocument();
-    expect(within(ui.searchList.get()).getByText('no_results')).toBeInTheDocument();
+    expect(ui.searchResultsList.get()).toBeInTheDocument();
+    expect(within(ui.searchResultsList.get()).getByText('no_results')).toBeInTheDocument();
     await user.click(ui.searchClear.get());
 
     // Should show results based on input value
index 57001a7be6991cdfab54d68d43ebaee81492bc7e..b6740aa146682cbefcef33c947ab305ff2adac27 100644 (file)
@@ -32,7 +32,7 @@ import MultiValueInput from './MultiValueInput';
 import PrimitiveInput from './PrimitiveInput';
 import PropertySetInput from './PropertySetInput';
 
-export default function Input(props: DefaultInputProps) {
+export default function Input(props: Readonly<DefaultInputProps>) {
   const { setting } = props;
   const { definition } = setting;
   const name = getUniqueName(definition);
index 1c1643fcc0093b3e9d865175a0df47e2bf399f9b..4886d7b1c26cdbc2833b900335bc8681bcdb29f1 100644 (file)
@@ -17,8 +17,9 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { Note, Switch } from 'design-system';
 import * as React from 'react';
-import Toggle, { getToggleValue } from '../../../../components/controls/Toggle';
+import { getToggleValue } from '../../../../components/controls/Toggle';
 import { translate } from '../../../../helpers/l10n';
 import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';
 
@@ -29,15 +30,20 @@ interface Props extends DefaultSpecializedInputProps {
 export default function InputForBoolean({ onChange, name, value, setting }: Props) {
   const toggleValue = getToggleValue(value != null ? value : false);
 
+  const propertyName = getPropertyName(setting.definition);
+
   return (
-    <div className="display-inline-block text-top">
-      <Toggle
+    <div className="sw-flex sw-items-center">
+      <Switch
         name={name}
         onChange={onChange}
         value={toggleValue}
-        ariaLabel={getPropertyName(setting.definition)}
+        labels={{
+          on: propertyName,
+          off: propertyName,
+        }}
       />
-      {value == null && <span className="spacer-left note">{translate('settings.not_set')}</span>}
+      {value == null && <Note className="sw-ml-2">{translate('settings.not_set')}</Note>}
     </div>
   );
 }
index a1ef8a82097c15e360c6720826f647b0d775f967..1ca1ed1b0c0f64859e4065410239ae7752da0325 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { InputTextArea } from 'design-system/lib';
 import * as React from 'react';
 import FormattingTipsWithLink from '../../../../components/common/FormattingTipsWithLink';
 import { Button } from '../../../../components/controls/buttons';
@@ -36,33 +37,30 @@ export default function InputForFormattedText(props: DefaultSpecializedInputProp
     props.onChange(event.target.value);
   }
 
-  return (
+  return editMode ? (
     <div>
-      {editMode ? (
-        <div className="display-flex-row">
-          <textarea
-            aria-label={getPropertyName(setting.definition)}
-            className="settings-large-input text-top spacer-right"
-            name={name}
-            onChange={handleInputChange}
-            rows={5}
-            value={value || ''}
-          />
-          <FormattingTipsWithLink className="abs-width-100" />
-        </div>
-      ) : (
-        <>
-          <div
-            className="markdown-preview markdown"
-            // eslint-disable-next-line react/no-danger
-            dangerouslySetInnerHTML={{ __html: sanitizeUserInput(formattedValue ?? '') }}
-          />
-          <Button className="spacer-top" onClick={props.onEditing}>
-            <EditIcon className="spacer-right" />
-            {translate('edit')}
-          </Button>
-        </>
-      )}
+      <InputTextArea
+        size="large"
+        aria-label={getPropertyName(setting.definition)}
+        className="settings-large-input text-top spacer-right"
+        name={name}
+        onChange={handleInputChange}
+        rows={5}
+        value={value || ''}
+      />
+      <FormattingTipsWithLink className="sw-mt-2" />
     </div>
+  ) : (
+    <>
+      <div
+        className="markdown-preview markdown"
+        // eslint-disable-next-line react/no-danger
+        dangerouslySetInnerHTML={{ __html: sanitizeUserInput(formattedValue ?? '') }}
+      />
+      <Button className="spacer-top" onClick={props.onEditing}>
+        <EditIcon className="spacer-right" />
+        {translate('edit')}
+      </Button>
+    </>
   );
 }
index 15d45a9a1593f632ac4f72ec3ae07ceb519ff3fc..e8146b628e900084dd2d2478f1350bf82c049b84 100644 (file)
@@ -17,9 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ButtonPrimary, FlagMessage, InputTextArea } from 'design-system/lib';
 import * as React from 'react';
-import { Button } from '../../../../components/controls/buttons';
-import { Alert } from '../../../../components/ui/Alert';
 import { translate } from '../../../../helpers/l10n';
 import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';
 
@@ -49,26 +48,33 @@ export default class InputForJSON extends React.PureComponent<DefaultSpecialized
   };
 
   render() {
-    const { value, name, setting } = this.props;
+    const { value, name, setting, isInvalid } = this.props;
     const { formatError } = this.state;
 
     return (
-      <div className="display-flex-end">
-        <textarea
-          className="settings-large-input text-top monospaced spacer-right"
-          name={name}
-          onChange={this.handleInputChange}
-          rows={5}
-          value={value || ''}
-          aria-label={getPropertyName(setting.definition)}
-        />
-        <div>
-          {formatError && <Alert variant="info">{translate('settings.json.format_error')} </Alert>}
-          <Button className="spacer-top" onClick={this.format}>
-            {translate('settings.json.format')}
-          </Button>
+      <>
+        <div className="sw-flex sw-items-end">
+          <InputTextArea
+            size="large"
+            name={name}
+            onChange={this.handleInputChange}
+            rows={5}
+            value={value || ''}
+            aria-label={getPropertyName(setting.definition)}
+            isInvalid={isInvalid}
+          />
+          <div className="sw-ml-2">
+            <ButtonPrimary className="sw-mt-2" onClick={this.format}>
+              {translate('settings.json.format')}
+            </ButtonPrimary>
+          </div>
         </div>
-      </div>
+        {formatError && (
+          <FlagMessage className="sw-mt-2" variant="warning">
+            {translate('settings.json.format_error')}{' '}
+          </FlagMessage>
+        )}
+      </>
     );
   }
 }
index b5892fe35aef1e4def4c85439353121976a80177..cf0a9c3bff909eaa7dfe6e9345609a6ae0f58a08 100644 (file)
@@ -22,5 +22,5 @@ import { DefaultSpecializedInputProps } from '../../utils';
 import SimpleInput from './SimpleInput';
 
 export default function InputForNumber(props: DefaultSpecializedInputProps) {
-  return <SimpleInput className="input-small" type="text" {...props} />;
+  return <SimpleInput size="small" type="text" {...props} />;
 }
index 0e478c36da742834a9437bbe3604aac359a7c812..30652f468a929d4824344853d3f804a36dd21906 100644 (file)
@@ -22,7 +22,5 @@ import { DefaultSpecializedInputProps } from '../../utils';
 import SimpleInput from './SimpleInput';
 
 export default function InputForPassword(props: DefaultSpecializedInputProps) {
-  return (
-    <SimpleInput {...props} className="settings-large-input" type="password" autoComplete="off" />
-  );
+  return <SimpleInput {...props} size="large" type="password" autoComplete="off" />;
 }
index 2a7294408287927a9ae3fd6055ef79b1e54d0abb..0262e59d35e862dc26ed96103a81cd14980deee1 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ButtonSecondary, LockIcon } from 'design-system';
 import * as React from 'react';
-import { colors } from '../../../../app/theme';
-import { Button } from '../../../../components/controls/buttons';
-import LockIcon from '../../../../components/icons/LockIcon';
 import { translate } from '../../../../helpers/l10n';
 import {
   DefaultInputProps,
@@ -74,15 +72,16 @@ export default class InputForSecured extends React.PureComponent<Props, State> {
     return (
       // The input hidden will prevent browser asking for saving login information
       <>
-        <input className="hidden" type="password" />
+        <input aria-hidden className="sw-hidden" tabIndex={-1} type="password" />
         <Input
           aria-label={getPropertyName(setting.definition)}
           autoComplete="off"
-          className="js-setting-input settings-large-input"
+          className="js-setting-input"
           isDefault={isDefaultOrInherited(setting)}
           name={name}
           onChange={this.handleInputChange}
           setting={setting}
+          size="large"
           type="password"
           value={value}
         />
@@ -96,12 +95,12 @@ export default class InputForSecured extends React.PureComponent<Props, State> {
     }
 
     return (
-      <>
-        <LockIcon className="text-middle big-spacer-right" fill={colors.gray60} />
-        <Button className="text-middle" onClick={this.handleChangeClick}>
+      <div className="sw-flex sw-items-center">
+        <LockIcon className="sw-mr-4" />
+        <ButtonSecondary onClick={this.handleChangeClick}>
           {translate('change_verb')}
-        </Button>
-      </>
+        </ButtonSecondary>
+      </div>
     );
   }
 }
index ec677d8d9da7cbb847cb5690c2846e4b588c1772..9b388acde7ed81f444f9c975f6233a4e2cadd0df 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { InputSelect } from 'design-system';
 import * as React from 'react';
-import Select from '../../../../components/controls/Select';
 import { ExtendedSettingDefinition } from '../../../../types/settings';
 import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';
 
 type Props = DefaultSpecializedInputProps & Pick<ExtendedSettingDefinition, 'options'>;
 
 export default class InputForSingleSelectList extends React.PureComponent<Props> {
-  handleInputChange = ({ value }: { value: string }) => {
+  handleInputChange = ({ value }: { label: string; value: string }) => {
     this.props.onChange(value);
   };
 
@@ -38,8 +38,7 @@ export default class InputForSingleSelectList extends React.PureComponent<Props>
     }));
 
     return (
-      <Select
-        className="settings-large-input"
+      <InputSelect
         name={name}
         onChange={this.handleInputChange}
         aria-label={getPropertyName(setting.definition)}
index 53fa59e7abdea02ea6486f1d98ed58f5e7686f7b..84fe8beac90b877afe0b44e6ff3bd814ccee35dc 100644 (file)
@@ -22,5 +22,5 @@ import { DefaultSpecializedInputProps } from '../../utils';
 import SimpleInput from './SimpleInput';
 
 export default function InputForString(props: DefaultSpecializedInputProps) {
-  return <SimpleInput className="settings-large-input" type="text" {...props} />;
+  return <SimpleInput size="large" type="text" {...props} />;
 }
index cc81005adebb48ebfa0c9c9d09c0ff182a08676c..e85741b0c5e155a7caa224fadac6064f876e7d4b 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { InputTextArea } from 'design-system';
 import * as React from 'react';
 import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';
 
@@ -28,8 +29,8 @@ export default class InputForText extends React.PureComponent<DefaultSpecialized
   render() {
     const { setting, name, value } = this.props;
     return (
-      <textarea
-        className="settings-large-input text-top"
+      <InputTextArea
+        size="large"
         name={name}
         onChange={this.handleInputChange}
         rows={5}
index b51035e164082264fc184380c151f625b5c20162..2ef8334abf8f4491d2fd1eee82ca399b8d822c7f 100644 (file)
@@ -17,8 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { DestructiveIcon, TrashIcon } from 'design-system';
 import * as React from 'react';
-import { DeleteButton } from '../../../../components/controls/buttons';
 import { translateWithParameters } from '../../../../helpers/l10n';
 import { DefaultSpecializedInputProps, getEmptyValue, getPropertyName } from '../../utils';
 import PrimitiveInput from './PrimitiveInput';
@@ -43,7 +43,7 @@ export default class MultiValueInput extends React.PureComponent<DefaultSpeciali
   renderInput(value: any, index: number, isLast: boolean) {
     const { setting, isDefault, name } = this.props;
     return (
-      <li className="spacer-bottom" key={index}>
+      <li className="sw-flex sw-items-center sw-mb-2" key={index}>
         <PrimitiveInput
           isDefault={isDefault}
           name={name}
@@ -54,8 +54,9 @@ export default class MultiValueInput extends React.PureComponent<DefaultSpeciali
         />
 
         {!isLast && (
-          <div className="display-inline-block spacer-left">
-            <DeleteButton
+          <div className="sw-inline-block sw-ml-2">
+            <DestructiveIcon
+              Icon={TrashIcon}
               className="js-remove-value"
               aria-label={translateWithParameters(
                 'settings.definition.delete_value',
index 645fef455c903012b70b6d44e03bb0e7f1f9c978..8c9bd9d96434637b0ed4de34a98be2a768ff5087 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import {
+  ActionCell,
+  ContentCell,
+  DestructiveIcon,
+  Note,
+  Table,
+  TableRow,
+  TrashIcon,
+} from 'design-system';
 import * as React from 'react';
-import { DeleteButton } from '../../../../components/controls/buttons';
 import { translateWithParameters } from '../../../../helpers/l10n';
 import {
   DefaultSpecializedInputProps,
@@ -53,7 +61,7 @@ export default class PropertySetInput extends React.PureComponent<DefaultSpecial
     const { definition } = setting;
 
     return (
-      <tr key={index}>
+      <TableRow key={index}>
         {isCategoryDefinition(definition) &&
           definition.fields.map((field) => {
             const newSetting = {
@@ -62,21 +70,23 @@ export default class PropertySetInput extends React.PureComponent<DefaultSpecial
               value: fieldValues[field.key],
             };
             return (
-              <td key={field.key}>
+              <ContentCell className="sw-py-2 sw-border-0" key={field.key}>
                 <PrimitiveInput
                   isDefault={isDefault}
                   hasValueChanged={this.props.hasValueChanged}
                   name={getUniqueName(definition, field.key)}
                   onChange={(value) => this.handleInputChange(index, field.key, value)}
                   setting={newSetting}
+                  size="auto"
                   value={fieldValues[field.key]}
                 />
-              </td>
+              </ContentCell>
             );
           })}
-        <td className="thin nowrap text-middle">
+        <ActionCell className="sw-border-0">
           {!isLast && (
-            <DeleteButton
+            <DestructiveIcon
+              Icon={TrashIcon}
               aria-label={translateWithParameters(
                 'settings.definitions.delete_fields',
                 getPropertyName(setting.definition),
@@ -86,8 +96,8 @@ export default class PropertySetInput extends React.PureComponent<DefaultSpecial
               onClick={() => this.handleDeleteValue(index)}
             />
           )}
-        </td>
-      </tr>
+        </ActionCell>
+      </TableRow>
     );
   }
 
@@ -95,32 +105,40 @@ export default class PropertySetInput extends React.PureComponent<DefaultSpecial
     const { definition } = this.props.setting;
     const displayedValue = [...this.ensureValue(), ...getEmptyValue(definition)];
 
+    const columnWidths = (isCategoryDefinition(definition) ? definition.fields : [])
+      .map(() => 'auto')
+      .concat('1px');
+
     return (
       <div>
-        <table
-          className="data zebra-hover no-outer-padding"
-          style={{ width: 'auto', minWidth: 480, marginTop: -12 }}
-        >
-          <thead>
-            <tr>
+        <Table
+          header={
+            <TableRow>
               {isCategoryDefinition(definition) &&
                 definition.fields.map((field) => (
-                  <th key={field.key}>
-                    {field.name}
-                    {field.description != null && (
-                      <span className="spacer-top small">{field.description}</span>
-                    )}
-                  </th>
+                  <ContentCell key={field.key}>
+                    <div className="sw-text-start sw-h-full">
+                      {field.name}
+                      {field.description != null && (
+                        <Note as="p" className="sw-mt-2">
+                          {field.description}
+                        </Note>
+                      )}
+                    </div>
+                  </ContentCell>
                 ))}
-              <th>&nbsp;</th>
-            </tr>
-          </thead>
-          <tbody>
-            {displayedValue.map((fieldValues, index) =>
-              this.renderFields(fieldValues, index, index === displayedValue.length - 1),
-            )}
-          </tbody>
-        </table>
+              <ContentCell />
+            </TableRow>
+          }
+          columnCount={columnWidths.length}
+          columnWidths={columnWidths}
+          noHeaderTopBorder
+          noSidePadding
+        >
+          {displayedValue.map((fieldValues, index) =>
+            this.renderFields(fieldValues, index, index === displayedValue.length - 1),
+          )}
+        </Table>
       </div>
     );
   }
index 4ca4a1526b7519c1eff3ab1c6dfa33aaf2e06cb9..84aedd90aa29751a5896a21ba7e60e311eddeac4 100644 (file)
@@ -17,7 +17,7 @@
  * 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 { InputField } from 'design-system';
 import * as React from 'react';
 import { KeyboardKeys } from '../../../../helpers/keycodes';
 import { DefaultSpecializedInputProps, getPropertyName } from '../../utils';
@@ -40,17 +40,29 @@ export default class SimpleInput extends React.PureComponent<SimpleInputProps> {
   };
 
   render() {
-    const { autoComplete, autoFocus, className, name, value = '', setting, type } = this.props;
+    const {
+      autoComplete,
+      autoFocus,
+      className,
+      isInvalid,
+      name,
+      value = '',
+      setting,
+      size,
+      type,
+    } = this.props;
     return (
-      <input
+      <InputField
+        isInvalid={isInvalid}
         autoComplete={autoComplete}
         autoFocus={autoFocus}
-        className={classNames('text-top', className)}
+        className={className}
         name={name}
         onChange={this.handleInputChange}
         onKeyDown={this.handleKeyDown}
         type={type}
         value={value}
+        size={size}
         aria-label={getPropertyName(setting.definition)}
       />
     );
index 0b97d1129b009095a0ea2e6c8e3ef131fe6a17c6..de956ccca47bf544605fb101d3372be1b85b473f 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { FlagMessage, InputField, Note, SubHeading, Switch } from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import withAvailableFeatures, {
   WithAvailableFeaturesProps,
 } from '../../../../app/components/available-features/withAvailableFeatures';
 import DocLink from '../../../../components/common/DocLink';
-import Toggle from '../../../../components/controls/Toggle';
-import { Alert } from '../../../../components/ui/Alert';
 import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
 import { ALM_DOCUMENTATION_PATHS } from '../../../../helpers/constants';
 import { translate } from '../../../../helpers/l10n';
@@ -63,12 +62,12 @@ function renderFieldWrapper(
   help?: React.ReactNode,
 ) {
   return (
-    <div className="settings-definition">
-      <div className="settings-definition-left">
-        {label}
-        {help && <div className="markdown small spacer-top">{help}</div>}
+    <div className="sw-p-6 sw-flex sw-gap-12">
+      <div className="sw-w-abs-300">
+        <SubHeading>{label}</SubHeading>
+        {help && <div className="markdown">{help}</div>}
       </div>
-      <div className="settings-definition-right padded-top">{input}</div>
+      <div className="sw-flex-1">{input}</div>
     </div>
   );
 }
@@ -83,7 +82,7 @@ function renderHelp({ help, helpExample, helpParams = {}, id }: CommonFieldProps
           values={helpParams}
         />
         {helpExample && (
-          <div className="spacer-top nowrap">
+          <div className="sw-mt-2 sw-whitespace-nowrap">
             {translate('example')}: <em>{helpExample}</em>
           </div>
         )}
@@ -95,10 +94,12 @@ function renderHelp({ help, helpExample, helpParams = {}, id }: CommonFieldProps
 function renderLabel(props: LabelProps) {
   const { optional, id } = props;
   return (
-    <label className="h3" htmlFor={id}>
-      {translate('settings.pr_decoration.binding.form', id)}
-      {!optional && <MandatoryFieldMarker />}
-    </label>
+    <SubHeading>
+      <label htmlFor={id}>
+        {translate('settings.pr_decoration.binding.form', id)}
+        {!optional && <MandatoryFieldMarker />}
+      </label>
+    </SubHeading>
   );
 }
 
@@ -109,13 +110,19 @@ function renderBooleanField(
   },
 ) {
   const { id, value, onFieldChange, propKey, inputExtra } = props;
+
+  const label = translate('settings.pr_decoration.binding.form', id);
+
   return renderFieldWrapper(
     renderLabel({ ...props, optional: true }),
-    <div className="display-flex-center big-spacer-top">
-      <div className="display-inline-block text-top">
-        <Toggle id={id} name={id} onChange={(v) => onFieldChange(propKey, v)} value={value} />
-        {value == null && <span className="spacer-left note">{translate('settings.not_set')}</span>}
-      </div>
+    <div className="sw-flex sw-items-start">
+      <Switch
+        name={id}
+        labels={{ on: label, off: label }}
+        onChange={(v) => onFieldChange(propKey, v)}
+        value={value}
+      />
+      {value == null && <Note className="sw-ml-2">{translate('settings.not_set')}</Note>}
       {inputExtra}
     </div>,
     renderHelp(props),
@@ -130,12 +137,12 @@ function renderField(
   const { id, propKey, value, onFieldChange } = props;
   return renderFieldWrapper(
     renderLabel(props),
-    <input
-      className="input-super-large big-spacer-top"
+    <InputField
       id={id}
       maxLength={256}
       name={id}
       onChange={(e) => onFieldChange(propKey, e.currentTarget.value)}
+      size="large"
       type="text"
       value={value}
     />,
@@ -165,6 +172,7 @@ export function AlmSpecificForm(props: AlmSpecificFormProps) {
             propKey: 'slug',
             value: slug || '',
           })}
+
           {renderField({
             help: true,
             helpExample: <strong>My Repository</strong>,
@@ -195,6 +203,7 @@ export function AlmSpecificForm(props: AlmSpecificFormProps) {
             propKey: 'repository',
             value: repository || '',
           })}
+
           {renderField({
             help: true,
             helpExample: (
@@ -251,6 +260,7 @@ export function AlmSpecificForm(props: AlmSpecificFormProps) {
             propKey: 'repository',
             value: repository || '',
           })}
+
           {renderBooleanField({
             help: true,
             id: 'github.summary_comment_setting',
@@ -295,9 +305,9 @@ export function AlmSpecificForm(props: AlmSpecificFormProps) {
           propKey: 'monorepo',
           value: monorepo,
           inputExtra: monorepo && (
-            <Alert className="no-margin-bottom spacer-left" variant="warning" display="inline">
+            <FlagMessage className="sw-ml-2" variant="warning">
               {translate('settings.pr_decoration.binding.form.monorepo.warning')}
-            </Alert>
+            </FlagMessage>
           ),
         })}
     </>
index ece36a858b3ecda6bffec004965c4f6f5c61504b..440ce44403c123143d9b79008e6e08a8dee36036 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import {
+  BasicSeparator,
+  ButtonPrimary,
+  ButtonSecondary,
+  FlagMessage,
+  Link,
+  Note,
+  Spinner,
+  SubHeading,
+  SubTitle,
+} from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import Link from '../../../../components/common/Link';
-import { Button, SubmitButton } from '../../../../components/controls/buttons';
 import AlmSettingsInstanceSelector from '../../../../components/devops-platform/AlmSettingsInstanceSelector';
-import AlertSuccessIcon from '../../../../components/icons/AlertSuccessIcon';
-import { Alert } from '../../../../components/ui/Alert';
 import MandatoryFieldMarker from '../../../../components/ui/MandatoryFieldMarker';
 import MandatoryFieldsExplanation from '../../../../components/ui/MandatoryFieldsExplanation';
-import Spinner from '../../../../components/ui/Spinner';
 import { translate } from '../../../../helpers/l10n';
 import { getGlobalSettingsUrl } from '../../../../helpers/urls';
 import {
@@ -78,39 +84,38 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe
   if (instances.length < 1) {
     return (
       <div>
-        <Alert className="spacer-top huge-spacer-bottom" variant="info">
+        <FlagMessage variant="info">
           {isSysAdmin ? (
-            <FormattedMessage
-              defaultMessage={translate('settings.pr_decoration.binding.no_bindings.admin')}
-              id="settings.pr_decoration.binding.no_bindings.admin"
-              values={{
-                link: (
-                  <Link to={getGlobalSettingsUrl(ALM_INTEGRATION_CATEGORY)}>
-                    {translate('settings.pr_decoration.binding.no_bindings.link')}
-                  </Link>
-                ),
-              }}
-            />
+            <p>
+              <FormattedMessage
+                defaultMessage={translate('settings.pr_decoration.binding.no_bindings.admin')}
+                id="settings.pr_decoration.binding.no_bindings.admin"
+                values={{
+                  link: (
+                    <Link to={getGlobalSettingsUrl(ALM_INTEGRATION_CATEGORY)}>
+                      {translate('settings.pr_decoration.binding.no_bindings.link')}
+                    </Link>
+                  ),
+                }}
+              />
+            </p>
           ) : (
             translate('settings.pr_decoration.binding.no_bindings')
           )}
-        </Alert>
+        </FlagMessage>
       </div>
     );
   }
 
-  const selected = formData.key && instances.find((i) => i.key === formData.key);
-  const alm = selected && selected.alm;
+  const selected = formData.key ? instances.find((i) => i.key === formData.key) : undefined;
 
   return (
-    <div>
-      <header className="page-header">
-        <h1 className="page-title">{translate('settings.pr_decoration.binding.title')}</h1>
-      </header>
+    <div className="sw-p-6">
+      <SubTitle as="h3">{translate('settings.pr_decoration.binding.title')}</SubTitle>
 
-      <div className="markdown small spacer-top big-spacer-bottom">
-        {translate('settings.pr_decoration.binding.description')}
-      </div>
+      <Note className="markdown">{translate('settings.pr_decoration.binding.description')}</Note>
+
+      <BasicSeparator className="sw-my-6" />
 
       <form
         onSubmit={(event: React.SyntheticEvent<HTMLFormElement>) => {
@@ -118,105 +123,117 @@ export default function PRDecorationBindingRenderer(props: PRDecorationBindingRe
           props.onSubmit();
         }}
       >
-        <MandatoryFieldsExplanation className="form-field" />
+        <MandatoryFieldsExplanation />
 
-        <div className="settings-definition big-spacer-bottom">
-          <div className="settings-definition-left">
-            <label className="h3" htmlFor="name">
-              {translate('settings.pr_decoration.binding.form.name')}
-              <MandatoryFieldMarker className="spacer-right" />
-            </label>
-            <div className="markdown small spacer-top">
+        <div className="sw-p-6 sw-flex sw-gap-12">
+          <div className="sw-w-abs-300">
+            <SubHeading>
+              <label htmlFor="name">
+                {translate('settings.pr_decoration.binding.form.name')}
+                <MandatoryFieldMarker className="sw-mr-2" />
+              </label>
+            </SubHeading>
+            <div className="markdown">
               {translate('settings.pr_decoration.binding.form.name.help')}
             </div>
           </div>
-          <div className="settings-definition-right">
+          <div className="sw-flex-1">
             <AlmSettingsInstanceSelector
               instances={instances}
               onChange={(instance: AlmSettingsInstance) => props.onFieldChange('key', instance.key)}
               initialValue={formData.key}
-              className="sw-w-abs-400 sw-mt-4 it__configuration-name-select"
+              className="sw-w-abs-400 it__configuration-name-select"
               inputId="name"
             />
           </div>
         </div>
 
-        {alm && (
+        {selected?.alm && (
           <AlmSpecificForm
-            alm={alm}
+            alm={selected.alm}
             instances={instances}
             formData={formData}
             onFieldChange={props.onFieldChange}
           />
         )}
 
-        <div className="display-flex-center big-spacer-top action-section">
+        <div className="sw-flex sw-items-center sw-mt-8 sw-gap-2">
           {isChanged && (
-            <SubmitButton className="spacer-right button-success" disabled={updating || !isValid}>
-              <span data-test="project-settings__alm-save">{translate('save')}</span>
-              <Spinner className="spacer-left" loading={updating} />
-            </SubmitButton>
+            <>
+              <ButtonPrimary disabled={updating || !isValid} type="submit">
+                <span data-test="project-settings__alm-save">{translate('save')}</span>
+              </ButtonPrimary>
+              <Spinner loading={updating} />
+            </>
           )}
           {!updating && successfullyUpdated && (
-            <span className="text-success spacer-right">
-              <AlertSuccessIcon className="spacer-right" />
-              {translate('settings.state.saved')}
-            </span>
+            <FlagMessage variant="success">{translate('settings.state.saved')}</FlagMessage>
           )}
           {isConfigured && (
             <>
-              <Button className="spacer-right" onClick={props.onReset}>
+              <ButtonSecondary onClick={props.onReset}>
                 <span data-test="project-settings__alm-reset">{translate('reset_verb')}</span>
-              </Button>
+              </ButtonSecondary>
               {!isChanged && (
-                <Button onClick={props.onCheckConfiguration} disabled={checkingConfiguration}>
-                  {translate('settings.pr_decoration.binding.check_configuration')}
-                  <Spinner className="spacer-left" loading={checkingConfiguration} />
-                </Button>
+                <>
+                  <ButtonSecondary
+                    onClick={props.onCheckConfiguration}
+                    disabled={checkingConfiguration}
+                  >
+                    {translate('settings.pr_decoration.binding.check_configuration')}
+                  </ButtonSecondary>
+                  <Spinner loading={checkingConfiguration} />
+                </>
               )}
             </>
           )}
         </div>
         {!checkingConfiguration && configurationErrors?.errors && (
-          <Alert variant="error" display="inline" className="big-spacer-top">
-            <p className="spacer-bottom">
-              {translate('settings.pr_decoration.binding.check_configuration.failure')}
-            </p>
-            <ul className="list-styled">
-              {configurationErrors.errors.map((error, i) => (
-                // eslint-disable-next-line react/no-array-index-key
-                <li key={i}>{error.msg}</li>
-              ))}
-            </ul>
-            {configurationErrors.scope === ProjectAlmBindingConfigurationErrorScope.Global && (
-              <p>
-                {isSysAdmin ? (
-                  <FormattedMessage
-                    id="settings.pr_decoration.binding.check_configuration.failure.check_global_settings"
-                    defaultMessage={translate(
-                      'settings.pr_decoration.binding.check_configuration.failure.check_global_settings',
-                    )}
-                    values={{
-                      link: (
-                        <Link to={getGlobalSettingsUrl(ALM_INTEGRATION_CATEGORY, { alm })}>
-                          {translate(
-                            'settings.pr_decoration.binding.check_configuration.failure.check_global_settings.link',
-                          )}
-                        </Link>
-                      ),
-                    }}
-                  />
-                ) : (
-                  translate('settings.pr_decoration.binding.check_configuration.contact_admin')
-                )}
+          <FlagMessage variant="error" className="sw-mt-6">
+            <div>
+              <p className="sw-mb-2">
+                {translate('settings.pr_decoration.binding.check_configuration.failure')}
               </p>
-            )}
-          </Alert>
+              <ul className="list-styled">
+                {configurationErrors.errors.map((error, i) => (
+                  // eslint-disable-next-line react/no-array-index-key
+                  <li key={i}>{error.msg}</li>
+                ))}
+              </ul>
+              {configurationErrors.scope === ProjectAlmBindingConfigurationErrorScope.Global && (
+                <p>
+                  {isSysAdmin ? (
+                    <FormattedMessage
+                      id="settings.pr_decoration.binding.check_configuration.failure.check_global_settings"
+                      defaultMessage={translate(
+                        'settings.pr_decoration.binding.check_configuration.failure.check_global_settings',
+                      )}
+                      values={{
+                        link: (
+                          <Link
+                            to={getGlobalSettingsUrl(ALM_INTEGRATION_CATEGORY, {
+                              alm: selected?.alm,
+                            })}
+                          >
+                            {translate(
+                              'settings.pr_decoration.binding.check_configuration.failure.check_global_settings.link',
+                            )}
+                          </Link>
+                        ),
+                      }}
+                    />
+                  ) : (
+                    translate('settings.pr_decoration.binding.check_configuration.contact_admin')
+                  )}
+                </p>
+              )}
+            </div>
+          </FlagMessage>
         )}
         {isConfigured && !isChanged && !checkingConfiguration && !configurationErrors && (
-          <Alert variant="success" display="inline" className="big-spacer-top">
+          <FlagMessage variant="success" className="sw-mt-6">
             {translate('settings.pr_decoration.binding.check_configuration.success')}
-          </Alert>
+          </FlagMessage>
         )}
       </form>
     </div>
index 56189e1f3c1d84a3e3fa4c24db124859379b89ea..7b02ab120e56c6f0844e606f65d2ea5508068911 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { InputSizeKeys } from 'design-system';
 import { sortBy } from 'lodash';
 import { Path } from 'react-router-dom';
 import { hasMessage, translate } from '../../helpers/l10n';
@@ -45,6 +46,8 @@ export type DefaultSpecializedInputProps = DefaultInputProps & {
 export interface DefaultInputProps {
   autoFocus?: boolean;
   isEditing?: boolean;
+  isInvalid?: boolean;
+  size?: InputSizeKeys;
   hasValueChanged?: boolean;
   onCancel?: () => void;
   onChange: (value: any) => void;
index 0faa3fa5e4a3dd9e473581d691012149c1701fc9..eb950cdeb226fb1713b06d00019672246eec6ed2 100644 (file)
@@ -17,8 +17,9 @@
  * 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 { CodeSnippet, Link } from 'design-system';
 import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
 import { translate } from '../../helpers/l10n';
 import { getFormattingHelpUrl } from '../../helpers/urls';
 
@@ -38,11 +39,27 @@ export default class FormattingTipsWithLink extends React.PureComponent<Props> {
 
   render() {
     return (
-      <div className={classNames('markdown-tips', this.props.className)}>
-        <a href="#" onClick={this.handleClick}>
+      <div className={this.props.className}>
+        <Link onClick={this.handleClick} to="#">
           {translate('formatting.helplink')}
-        </a>
-        <p className="spacer-top">{translate('formatting.example.link')}</p>
+        </Link>
+        <p className="sw-mt-2">
+          <FormattedMessage
+            id="formatting.example.link"
+            values={{
+              example: (
+                <>
+                  <br />
+                  <CodeSnippet
+                    isOneLine
+                    noCopy
+                    snippet={translate('formatting.example.link.example')}
+                  />
+                </>
+              ),
+            }}
+          />
+        </p>
       </div>
     );
   }
index c77e44ef4e596139ba114422315af069a620f713..8a2ae88b08bd4b38bf9bbe90782330ba9c2f144a 100644 (file)
@@ -3374,7 +3374,8 @@ sonarlint-connected-mode-doc=documentation about SonarLint Connected Mode
 #------------------------------------------------------------------------------
 formatting.page=Formatting
 formatting.helplink=Formatting Help
-formatting.example.link=For a hyperlink, write: [link label](https://www.domain.com)
+formatting.example.link=For a hyperlink, write: {example}
+formatting.example.link.example=[link label](https://www.domain.com)
 
 #------------------------------------------------------------------------------
 #