aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components/controls
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/components/controls')
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx27
-rw-r--r--server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx18
-rw-r--r--server/sonar-web/src/main/js/components/controls/Modal.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/controls/RadioCard.css115
-rw-r--r--server/sonar-web/src/main/js/components/controls/RadioCard.tsx77
-rw-r--r--server/sonar-web/src/main/js/components/controls/SimpleModal.tsx9
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx47
-rw-r--r--server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap128
8 files changed, 392 insertions, 34 deletions
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
index c3be977b72d..0718757ff1a 100644
--- a/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ConfirmButton.tsx
@@ -18,40 +18,25 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import ConfirmModal, { ConfirmModalProps } from './ConfirmModal';
import ModalButton, { ChildrenProps, ModalProps } from './ModalButton';
-import ConfirmModal from './ConfirmModal';
-export { ChildrenProps } from './ModalButton';
-
-interface Props {
+interface Props<T> extends ConfirmModalProps<T> {
children: (props: ChildrenProps) => React.ReactNode;
- cancelButtonText?: string;
- confirmButtonText: string;
- confirmData?: string;
- confirmDisable?: boolean;
- isDestructive?: boolean;
modalBody: React.ReactNode;
modalHeader: string;
- onConfirm: (data?: string) => void | Promise<void>;
}
interface State {
modal: boolean;
}
-export default class ConfirmButton extends React.PureComponent<Props, State> {
+export default class ConfirmButton<T> extends React.PureComponent<Props<T>, State> {
renderConfirmModal = ({ onClose }: ModalProps) => {
+ const { children, modalBody, modalHeader, ...confirmModalProps } = this.props;
return (
- <ConfirmModal
- cancelButtonText={this.props.cancelButtonText}
- confirmButtonText={this.props.confirmButtonText}
- confirmData={this.props.confirmData}
- confirmDisable={this.props.confirmDisable}
- header={this.props.modalHeader}
- isDestructive={this.props.isDestructive}
- onClose={onClose}
- onConfirm={this.props.onConfirm}>
- {this.props.modalBody}
+ <ConfirmModal header={modalHeader} onClose={onClose} {...confirmModalProps}>
+ {modalBody}
</ConfirmModal>
);
};
diff --git a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
index 8ac88d222ac..f51f9704aec 100644
--- a/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/ConfirmModal.tsx
@@ -18,21 +18,24 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { ModalProps } from './Modal';
import SimpleModal, { ChildrenProps } from './SimpleModal';
import DeferredSpinner from '../common/DeferredSpinner';
-import { translate } from '../../helpers/l10n';
import { SubmitButton, ResetButtonLink } from '../ui/buttons';
+import { translate } from '../../helpers/l10n';
-interface Props<T> {
- children: React.ReactNode;
+export interface ConfirmModalProps<T> extends ModalProps {
cancelButtonText?: string;
confirmButtonText: string;
confirmData?: T;
confirmDisable?: boolean;
- header: string;
isDestructive?: boolean;
+ onConfirm: (data?: T) => void | Promise<void | Response>;
+}
+
+interface Props<T> extends ConfirmModalProps<T> {
+ header: string;
onClose: () => void;
- onConfirm: (data?: T) => void | Promise<void>;
}
export default class ConfirmModal<T = string> extends React.PureComponent<Props<T>> {
@@ -82,9 +85,10 @@ export default class ConfirmModal<T = string> extends React.PureComponent<Props<
};
render() {
- const { header } = this.props;
+ const { header, onClose, medium, noBackdrop, large, simple } = this.props;
+ const modalProps = { header, onClose, medium, noBackdrop, large, simple };
return (
- <SimpleModal header={header} onClose={this.props.onClose} onSubmit={this.handleSubmit}>
+ <SimpleModal onSubmit={this.handleSubmit} {...modalProps}>
{this.renderModalContent}
</SimpleModal>
);
diff --git a/server/sonar-web/src/main/js/components/controls/Modal.tsx b/server/sonar-web/src/main/js/components/controls/Modal.tsx
index 7f91bb4e082..e7b14c3a1df 100644
--- a/server/sonar-web/src/main/js/components/controls/Modal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/Modal.tsx
@@ -23,7 +23,8 @@ import * as classNames from 'classnames';
ReactModal.setAppElement('#content');
-interface OwnProps {
+export interface ModalProps {
+ children: React.ReactNode;
medium?: boolean;
noBackdrop?: boolean;
large?: boolean;
@@ -32,7 +33,7 @@ interface OwnProps {
type MandatoryProps = Pick<ReactModal.Props, 'contentLabel'>;
-type Props = Partial<ReactModal.Props> & MandatoryProps & OwnProps;
+type Props = Partial<ReactModal.Props> & MandatoryProps & ModalProps;
export default function Modal(props: Props) {
return (
diff --git a/server/sonar-web/src/main/js/components/controls/RadioCard.css b/server/sonar-web/src/main/js/components/controls/RadioCard.css
new file mode 100644
index 00000000000..a5099a9a239
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/RadioCard.css
@@ -0,0 +1,115 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+.radio-card {
+ display: flex;
+ flex-direction: column;
+ width: 450px;
+ min-height: 210px;
+ background-color: #fff;
+ border: solid 1px var(--barBorderColor);
+ border-radius: 3px;
+ box-sizing: border-box;
+ margin-right: calc(2 * var(--gridSize));
+ transition: all 0.2s ease;
+}
+
+.radio-card.animated {
+ height: 0;
+ border-width: 0;
+ overflow: hidden;
+}
+
+.radio-card.animated.open {
+ height: 210px;
+ border-width: 1px;
+}
+
+.radio-card.highlight {
+ box-shadow: var(--defaultShadow);
+}
+
+.radio-card:last-child {
+ margin-right: 0;
+}
+
+.radio-card:focus {
+ outline: none;
+}
+
+.radio-card-actionable {
+ cursor: pointer;
+}
+
+.radio-card-actionable:not(.disabled):hover {
+ box-shadow: var(--defaultShadow);
+ transform: translateY(-2px);
+}
+
+.radio-card-actionable.selected {
+ border-color: var(--darkBlue);
+}
+
+.radio-card-actionable.selected .radio-card-recommended {
+ border: solid 1px var(--darkBlue);
+ border-top: none;
+}
+
+.radio-card-actionable.disabled {
+ cursor: not-allowed;
+ background-color: var(--disableGrayBg);
+ border-color: var(--disableGrayBorder);
+}
+
+.radio-card-actionable.disabled h2,
+.radio-card-actionable.disabled ul {
+ color: var(--disableGrayText);
+}
+
+.radio-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: calc(2 * var(--gridSize)) calc(2 * var(--gridSize)) 0;
+}
+
+.radio-card-body {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 0 calc(2 * var(--gridSize)) calc(2 * var(--gridSize));
+}
+
+.radio-card-body .alert {
+ margin-bottom: 0;
+}
+
+.radio-card-recommended {
+ position: relative;
+ padding: 6px calc(var(--gridSize) * 2);
+ left: -1px;
+ bottom: -1px;
+ width: 450px;
+ color: #fff;
+ background-color: var(--blue);
+ border-radius: 0 0 3px 3px;
+ box-sizing: border-box;
+ font-size: var(--smallFontSize);
+}
diff --git a/server/sonar-web/src/main/js/components/controls/RadioCard.tsx b/server/sonar-web/src/main/js/components/controls/RadioCard.tsx
new file mode 100644
index 00000000000..96726369abf
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/RadioCard.tsx
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import * as classNames from 'classnames';
+import { FormattedMessage } from 'react-intl';
+import RecommendedIcon from '../icons-components/RecommendedIcon';
+import { translate } from '../../helpers/l10n';
+import './RadioCard.css';
+
+export interface RadioCardProps {
+ className?: string;
+ disabled?: boolean;
+ onClick?: () => void;
+ selected?: boolean;
+}
+
+interface Props extends RadioCardProps {
+ children: React.ReactNode;
+ recommended?: string;
+ title: React.ReactNode;
+ titleInfo?: React.ReactNode;
+}
+
+export default function RadioCard(props: Props) {
+ const { className, disabled, onClick, recommended, selected, titleInfo } = props;
+ const isActionable = Boolean(onClick);
+ return (
+ <div
+ aria-checked={selected}
+ className={classNames(
+ 'radio-card',
+ { 'radio-card-actionable': isActionable, disabled, selected },
+ className
+ )}
+ onClick={isActionable && !disabled ? onClick : undefined}
+ role="radio"
+ tabIndex={0}>
+ <h2 className="radio-card-header big-spacer-bottom">
+ <span className="display-flex-center">
+ {isActionable && (
+ <i className={classNames('icon-radio', 'spacer-right', { 'is-checked': selected })} />
+ )}
+ {props.title}
+ </span>
+ {titleInfo}
+ </h2>
+ <div className="radio-card-body">{props.children}</div>
+ {recommended && (
+ <div className="radio-card-recommended">
+ <RecommendedIcon className="spacer-right" />
+ <FormattedMessage
+ defaultMessage={recommended}
+ id={recommended}
+ values={{ recommended: <strong>{translate('recommended')}</strong> }}
+ />
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx b/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
index a8a1ba1fe2d..9870f0e6ec7 100644
--- a/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
+++ b/server/sonar-web/src/main/js/components/controls/SimpleModal.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import Modal from './Modal';
+import Modal, { ModalProps } from './Modal';
export interface ChildrenProps {
onCloseClick: (event?: React.SyntheticEvent<HTMLElement>) => void;
@@ -27,7 +27,7 @@ export interface ChildrenProps {
submitting: boolean;
}
-interface Props {
+interface Props extends ModalProps {
children: (props: ChildrenProps) => React.ReactNode;
header: string;
onClose: () => void;
@@ -86,9 +86,10 @@ export default class SimpleModal extends React.Component<Props, State> {
};
render() {
+ const { children, header, onClose, onSubmit, ...modalProps } = this.props;
return (
- <Modal contentLabel={this.props.header} onRequestClose={this.props.onClose}>
- {this.props.children({
+ <Modal contentLabel={header} onRequestClose={onClose} {...modalProps}>
+ {children({
onCloseClick: this.handleCloseClick,
onFormSubmit: this.handleFormSubmit,
onSubmitClick: this.handleSubmitClick,
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx
new file mode 100644
index 00000000000..da11ea3458c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/RadioCard-test.tsx
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import RadioCard from '../RadioCard';
+import { click } from '../../../helpers/testUtils';
+
+it('should render correctly', () => {
+ expect(
+ shallow(
+ <RadioCard recommended="Recommended for you" title="Radio Card" titleInfo="info">
+ <div>content</div>
+ </RadioCard>
+ )
+ ).toMatchSnapshot();
+});
+
+it('should be actionable', () => {
+ const onClick = jest.fn();
+ const wrapper = shallow(
+ <RadioCard onClick={onClick} title="Radio Card">
+ <div>content</div>
+ </RadioCard>
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper);
+ wrapper.setProps({ selected: true, titleInfo: 'info' });
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap
new file mode 100644
index 00000000000..4394ef55ac3
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/RadioCard-test.tsx.snap
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should be actionable 1`] = `
+<div
+ className="radio-card radio-card-actionable"
+ onClick={[MockFunction]}
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ <i
+ className="icon-radio spacer-right"
+ />
+ Radio Card
+ </span>
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should be actionable 2`] = `
+<div
+ aria-checked={true}
+ className="radio-card radio-card-actionable selected"
+ onClick={
+ [MockFunction] {
+ "calls": Array [
+ Array [
+ Object {
+ "currentTarget": Object {
+ "blur": [Function],
+ },
+ "preventDefault": [Function],
+ "stopPropagation": [Function],
+ "target": Object {
+ "blur": [Function],
+ },
+ },
+ ],
+ ],
+ "results": Array [
+ Object {
+ "isThrow": false,
+ "value": undefined,
+ },
+ ],
+ }
+ }
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ <i
+ className="icon-radio spacer-right is-checked"
+ />
+ Radio Card
+ </span>
+ info
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+ className="radio-card"
+ role="radio"
+ tabIndex={0}
+>
+ <h2
+ className="radio-card-header big-spacer-bottom"
+ >
+ <span
+ className="display-flex-center"
+ >
+ Radio Card
+ </span>
+ info
+ </h2>
+ <div
+ className="radio-card-body"
+ >
+ <div>
+ content
+ </div>
+ </div>
+ <div
+ className="radio-card-recommended"
+ >
+ <RecommendedIcon
+ className="spacer-right"
+ />
+ <FormattedMessage
+ defaultMessage="Recommended for you"
+ id="Recommended for you"
+ values={
+ Object {
+ "recommended": <strong>
+ recommended
+ </strong>,
+ }
+ }
+ />
+ </div>
+</div>
+`;