name={name}
onClick={handleClick}
role="switch"
+ type="button"
>
<CheckIconContainer active={value} disabled={disabled}>
{value && <CheckIcon fill="currentColor" />}
`;
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`}
*/
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';
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}
>
);
}
-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`}
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;
* 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';
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) => ({
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>
);
}
* 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';
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;
+`;
* 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';
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 };
this.setState({ reseting: true });
};
- handleSubmit = () => {
+ handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
+ e.preventDefault();
this.props.onReset();
this.handleClose();
};
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>
);
}
}
* 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';
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>
);
* 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';
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;
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}
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}
* 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';
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
definition={setting.definition}
initialSettingValue={setting.settingValue}
/>
- </li>
+ </StyledListItem>
))}
</ul>
);
}
+
+const StyledListItem = styled.li`
+ & + & {
+ border-top: ${themeBorder('default')};
+ }
+`;
* 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';
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>
+ </>
);
}
}
* 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';
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);
});
};
+ 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}
+ />
)}
</>
);
* 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';
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 ? (
);
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>
);
}
* 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';
location: Location;
}
-function SettingsAppRenderer(props: SettingsAppRendererProps) {
+function SettingsAppRenderer(props: Readonly<SettingsAppRendererProps>) {
const { definitions, component, loading, location } = props;
const categories = React.useMemo(() => {
(!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;
+`;
import SettingsSearchRenderer from './SettingsSearchRenderer';
interface Props {
- className?: string;
component?: Component;
definitions: ExtendedSettingDefinition[];
router: Router;
};
render() {
- const { className, component } = this.props;
+ const { component } = this.props;
return (
<SettingsSearchRenderer
- className={className}
component={component}
onClickOutside={this.hideResults}
onMouseOverResult={this.handleMouseOverResult}
* 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;
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;
+ }
+`;
* 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';
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);
}
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' });
}
}
? 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>
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', () => {
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' }),
// 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
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);
* 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';
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>
);
}
* 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';
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>
+ </>
);
}
* 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';
};
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>
+ )}
+ </>
);
}
}
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} />;
}
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" />;
}
* 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,
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}
/>
}
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>
);
}
}
* 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);
};
}));
return (
- <Select
- className="settings-large-input"
+ <InputSelect
name={name}
onChange={this.handleInputChange}
aria-label={getPropertyName(setting.definition)}
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} />;
}
* 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';
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}
* 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';
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}
/>
{!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',
* 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,
const { definition } = setting;
return (
- <tr key={index}>
+ <TableRow key={index}>
{isCategoryDefinition(definition) &&
definition.fields.map((field) => {
const newSetting = {
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),
onClick={() => this.handleDeleteValue(index)}
/>
)}
- </td>
- </tr>
+ </ActionCell>
+ </TableRow>
);
}
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> </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>
);
}
* 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';
};
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)}
/>
);
* 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';
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>
);
}
values={helpParams}
/>
{helpExample && (
- <div className="spacer-top nowrap">
+ <div className="sw-mt-2 sw-whitespace-nowrap">
{translate('example')}: <em>{helpExample}</em>
</div>
)}
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>
);
}
},
) {
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),
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}
/>,
propKey: 'slug',
value: slug || '',
})}
+
{renderField({
help: true,
helpExample: <strong>My Repository</strong>,
propKey: 'repository',
value: repository || '',
})}
+
{renderField({
help: true,
helpExample: (
propKey: 'repository',
value: repository || '',
})}
+
{renderBooleanField({
help: true,
id: 'github.summary_comment_setting',
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>
),
})}
</>
* 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 {
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>) => {
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>
* 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';
export interface DefaultInputProps {
autoFocus?: boolean;
isEditing?: boolean;
+ isInvalid?: boolean;
+ size?: InputSizeKeys;
hasValueChanged?: boolean;
onCancel?: () => void;
onChange: (value: any) => void;
* 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';
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>
);
}
#------------------------------------------------------------------------------
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)
#------------------------------------------------------------------------------
#