From 52b02379a6f0485c9efd4bc59f05752a9cc0ed20 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Tue, 7 Sep 2021 15:44:58 +0200 Subject: [PATCH] Fix multiple Code Smells --- .../main/js/components/charts/BubbleChart.tsx | 20 +- .../main/js/components/charts/Histogram.tsx | 6 +- .../js/components/charts/ZoomTimeLine.tsx | 9 +- .../charts/__tests__/BubbleChart-test.tsx | 142 +++++++++-- .../charts/__tests__/Histogram-test.tsx | 70 +++--- .../__snapshots__/BubbleChart-test.tsx.snap | 238 +++++++----------- .../__snapshots__/Histogram-test.tsx.snap | 164 +++++++++++- .../js/components/controls/ConfirmModal.tsx | 4 +- .../main/js/components/controls/Dropdown.tsx | 3 - .../js/components/controls/GlobalMessages.tsx | 10 +- .../controls/InputValidationField.tsx | 4 +- .../js/components/controls/RadioToggle.tsx | 2 +- .../controls/ScreenPositionFixer.tsx | 20 +- .../main/js/components/controls/SearchBox.tsx | 10 +- .../main/js/components/controls/Select.tsx | 25 +- .../main/js/components/controls/Tooltip.css | 3 - .../main/js/components/controls/Tooltip.tsx | 6 +- .../controls/__tests__/Select-test.tsx | 81 ++++++ .../__snapshots__/Select-test.tsx.snap | 50 ++++ .../main/js/components/controls/buttons.tsx | 2 +- .../icons/OnboardingAddMembersIcon.tsx | 43 ---- .../components/icons/OnboardingTeamIcon.tsx | 34 --- .../main/js/components/icons/SeverityIcon.tsx | 4 +- .../main/js/components/icons/StatusIcon.tsx | 4 +- .../js/components/icons/TestStatusIcon.tsx | 4 +- .../src/main/js/components/ui/Alert.tsx | 5 +- .../main/js/components/ui/ContextNavBar.tsx | 6 +- .../src/main/js/components/ui/NavBar.css | 3 - .../src/main/js/components/ui/NavBarTabs.tsx | 5 +- .../mocks/dom.ts} | 28 ++- server/sonar-web/src/main/js/helpers/pages.ts | 2 +- 31 files changed, 616 insertions(+), 391 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx create mode 100644 server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/Select-test.tsx.snap delete mode 100644 server/sonar-web/src/main/js/components/icons/OnboardingAddMembersIcon.tsx delete mode 100644 server/sonar-web/src/main/js/components/icons/OnboardingTeamIcon.tsx rename server/sonar-web/src/main/js/{components/icons/OnboardingProjectIcon.tsx => helpers/mocks/dom.ts} (52%) diff --git a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx index 75d3172ea80..f71f476a50a 100644 --- a/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx +++ b/server/sonar-web/src/main/js/components/charts/BubbleChart.tsx @@ -102,7 +102,6 @@ export default class BubbleChart extends React.PureComponent, State> this.zoom = zoom() .scaleExtent([1, 10]) .on('zoom', this.zoomed); - // TODO: Check why as any cast is necessary now. select(this.node).call(this.zoom as any); }; @@ -118,11 +117,10 @@ export default class BubbleChart extends React.PureComponent, State> }); }; - resetZoom = (event: React.MouseEvent) => { - event.stopPropagation(); - event.preventDefault(); + resetZoom = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); if (this.zoom && this.node) { - // TODO: Check why as any cast is necessary now. select(this.node).call(this.zoom.transform as any, zoomIdentity); } }; @@ -144,11 +142,11 @@ export default class BubbleChart extends React.PureComponent, State> } getTicks(scale: Scale, format: (d: number) => string) { - const zoom = Math.ceil(this.state.transform.k); - const ticks = scale.ticks(TICKS_COUNT * zoom).map(tick => format(tick)); + const zoomAmount = Math.ceil(this.state.transform.k); + const ticks = scale.ticks(TICKS_COUNT * zoomAmount).map(tick => format(tick)); const uniqueTicksCount = uniq(ticks).length; const ticksCount = - uniqueTicksCount < TICKS_COUNT * zoom ? uniqueTicksCount - 1 : TICKS_COUNT * zoom; + uniqueTicksCount < TICKS_COUNT * zoomAmount ? uniqueTicksCount - 1 : TICKS_COUNT * zoomAmount; return scale.ticks(ticksCount); } @@ -360,10 +358,10 @@ interface BubbleProps { } function Bubble(props: BubbleProps) { - const handleClick = (event: React.MouseEvent) => { + const handleClick = (e: React.MouseEvent) => { if (props.onClick) { - event.stopPropagation(); - event.preventDefault(); + e.stopPropagation(); + e.preventDefault(); props.onClick(props.data); } }; diff --git a/server/sonar-web/src/main/js/components/charts/Histogram.tsx b/server/sonar-web/src/main/js/components/charts/Histogram.tsx index a4ae509a2ef..08b0f30e948 100644 --- a/server/sonar-web/src/main/js/components/charts/Histogram.tsx +++ b/server/sonar-web/src/main/js/components/charts/Histogram.tsx @@ -47,7 +47,7 @@ export default class Histogram extends React.PureComponent { const width = Math.round(xScale(d)) + /* minimum bar width */ 1; const x = xScale.range()[0] + (alignTicks ? padding[3] : 0); - const y = Math.round(yScale(index)! + yScale.bandwidth() / 2); + const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2); return ; } @@ -62,7 +62,7 @@ export default class Histogram extends React.PureComponent { } const x = xScale(d) + (alignTicks ? padding[3] : 0); - const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2); + const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2 + BAR_HEIGHT / 2); return ( @@ -83,7 +83,7 @@ export default class Histogram extends React.PureComponent { } const x = xScale.range()[0]; - const y = Math.round(yScale(index)! + yScale.bandwidth() / 2 + BAR_HEIGHT / 2); + const y = Math.round((yScale(index) || 0) + yScale.bandwidth() / 2 + BAR_HEIGHT / 2); const historyTickClass = alignTicks ? 'histogram-tick-start' : 'histogram-tick'; return ( diff --git a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx index f4f2742eb75..7caee5b3aec 100644 --- a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx +++ b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx @@ -49,7 +49,10 @@ interface State { } type XScale = ScaleTime; -// TODO it should be `ScaleLinear | ScalePoint | ScalePoint`, but it's super hard to make it work :'( +// It should be `ScaleLinear | ScalePoint | ScalePoint`, but in order +// to make it work, we need to write a lot of type guards :-(. This introduces a lot of unnecessary code, +// not to mention overhead at runtime. The simplest is just to cast to any, and rely on D3's internals +// to make it work. type YScale = any; export default class ZoomTimeLine extends React.PureComponent { @@ -114,7 +117,7 @@ export default class ZoomTimeLine extends React.PureComponent { this.handleZoomUpdate(xScale, xDim); }; - handleSelectionDrag = (xScale: XScale, width: number, xDim: number[], checkDelta?: boolean) => ( + handleSelectionDrag = (xScale: XScale, width: number, xDim: number[], checkDelta = false) => ( _: MouseEvent, data: DraggableData ) => { @@ -129,7 +132,7 @@ export default class ZoomTimeLine extends React.PureComponent { fixedX: number, xDim: number[], handleDirection: string, - checkDelta?: boolean + checkDelta = false ) => (_: MouseEvent, data: DraggableData) => { if (!checkDelta || data.deltaX) { const x = Math.max(xDim[0], Math.min(data.x, xDim[1])); diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx index 3be36c3677d..967b075314f 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx +++ b/server/sonar-web/src/main/js/components/charts/__tests__/BubbleChart-test.tsx @@ -17,43 +17,135 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { mount } from 'enzyme'; +import { select } from 'd3-selection'; +import { zoom } from 'd3-zoom'; +import { shallow } from 'enzyme'; import * as React from 'react'; -import { AutoSizerProps } from 'react-virtualized'; +import { Link } from 'react-router'; +import { AutoSizer, AutoSizerProps } from 'react-virtualized/dist/commonjs/AutoSizer'; +import { mockComponentMeasureEnhanced } from '../../../helpers/mocks/component'; +import { mockHtmlElement } from '../../../helpers/mocks/dom'; +import { mockEvent } from '../../../helpers/testMocks'; +import { click } from '../../../helpers/testUtils'; import BubbleChart from '../BubbleChart'; jest.mock('react-virtualized/dist/commonjs/AutoSizer', () => ({ AutoSizer: ({ children }: AutoSizerProps) => children({ width: 100, height: NaN }) })); +jest.mock('d3-selection', () => ({ + event: { transform: { x: 10, y: 10, k: 20 } }, + select: jest.fn().mockReturnValue({ call: jest.fn() }) +})); + +jest.mock('d3-zoom', () => ({ + ...jest.requireActual('d3-zoom'), + zoom: jest.fn() +})); + +beforeEach(jest.clearAllMocks); + it('should display bubbles', () => { - const items = [ - { x: 1, y: 10, size: 7 }, - { x: 2, y: 30, size: 5 } - ]; - const chart = mount(); - chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot()); - - chart.setProps({ height: 120 }); + const wrapper = shallowRender(); + wrapper + .find(AutoSizer) + .dive() + .find('Bubble') + .forEach(bubble => { + expect(bubble.dive()).toMatchSnapshot(); + }); }); it('should render bubble links', () => { - const items = [ - { x: 1, y: 10, size: 7, link: 'foo' }, - { x: 2, y: 30, size: 5, link: 'bar' } - ]; - const chart = mount(); - chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot()); + const wrapper = shallowRender({ + items: [ + { x: 1, y: 10, size: 7, link: 'foo' }, + { x: 2, y: 30, size: 5, link: 'bar' } + ] + }); + wrapper + .find(AutoSizer) + .dive() + .find('Bubble') + .forEach(bubble => { + expect(bubble.dive()).toMatchSnapshot(); + }); }); it('should render bubbles with click handlers', () => { - const onClick = jest.fn(); - const items = [ - { x: 1, y: 10, size: 7, data: 'foo' }, - { x: 2, y: 30, size: 5, data: 'bar' } - ]; - const chart = mount( - - ); - chart.find('Bubble').forEach(bubble => expect(bubble).toMatchSnapshot()); + const onBubbleClick = jest.fn(); + const wrapper = shallowRender({ onBubbleClick }); + wrapper + .find(AutoSizer) + .dive() + .find('Bubble') + .forEach(bubble => { + click(bubble.dive().find('circle')); + expect(bubble.dive()).toMatchSnapshot(); + }); + expect(onBubbleClick).toBeCalledTimes(2); + expect(onBubbleClick).toHaveBeenLastCalledWith(mockComponentMeasureEnhanced()); +}); + +it('should correctly handle zooming', () => { + class ZoomBehaviorMock { + on = () => this; + scaleExtent = () => this; + translateExtent = () => this; + } + + const call = jest.fn(); + const zoomBehavior = new ZoomBehaviorMock(); + (select as jest.Mock).mockReturnValueOnce({ call }); + (zoom as jest.Mock).mockReturnValueOnce(zoomBehavior); + + return new Promise((resolve, reject) => { + const wrapper = shallowRender({ padding: [5, 5, 5, 5] }); + wrapper.instance().boundNode( + mockHtmlElement({ + getBoundingClientRect: () => ({ width: 100, height: 100 } as DOMRect) + }) + ); + + // Call zoom event handler. + wrapper.instance().zoomed(); + expect(wrapper.state().transform).toEqual({ + x: 105, + y: 105, + k: 20 + }); + + // Reset Zoom levels. + const resetZoomClick = wrapper + .find('div.bubble-chart-zoom') + .find(Link) + .props().onClick; + if (!resetZoomClick) { + reject(); + return; + } + + const stopPropagation = jest.fn(); + const preventDefault = jest.fn(); + resetZoomClick(mockEvent({ stopPropagation, preventDefault })); + expect(stopPropagation).toBeCalled(); + expect(preventDefault).toBeCalled(); + expect(call).toHaveBeenCalledWith(zoomBehavior); + + resolve(); + }); }); + +function shallowRender(props: Partial['props']> = {}) { + return shallow>( + + ); +} diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx b/server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx index 15f4754aae0..2f67d166cfd 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx +++ b/server/sonar-web/src/main/js/components/charts/__tests__/Histogram-test.tsx @@ -17,52 +17,50 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { scaleBand } from 'd3-scale'; import { shallow } from 'enzyme'; import * as React from 'react'; import Histogram from '../Histogram'; -it('renders', () => { - expect(shallow()).toMatchSnapshot(); +jest.mock('d3-scale', () => { + const d3 = jest.requireActual('d3-scale'); + return { + ...d3, + scaleBand: jest.fn(d3.scaleBand) + }; }); -it('renders with yValues', () => { - expect( - shallow( - - ) - ).toMatchSnapshot(); -}); +beforeEach(jest.clearAllMocks); -it('renders with yValues and yTicks', () => { +it('renders correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ alignTicks: true })).toMatchSnapshot('align ticks'); + expect(shallowRender({ yValues: ['100.0', '75.0', '150.0'] })).toMatchSnapshot('with yValues'); expect( - shallow( - - ) - ).toMatchSnapshot(); + shallowRender({ yTicks: ['a', 'b', 'c'], yValues: ['100.0', '75.0', '150.0'] }) + ).toMatchSnapshot('with yValues and yTicks'); + expect( + shallowRender({ + yTicks: ['a', 'b', 'c'], + yTooltips: ['a - 100', 'b - 75', 'c - 150'], + yValues: ['100.0', '75.0', '150.0'] + }) + ).toMatchSnapshot('with yValues, yTicks and yTooltips'); }); -it('renders with yValues, yTicks and yTooltips', () => { +it('correctly handles yScale() returning undefined', () => { + const yScale = () => undefined; + yScale.bandwidth = () => 1; + + (scaleBand as jest.Mock).mockReturnValueOnce({ + domain: () => ({ rangeRound: () => yScale }) + }); + expect( - shallow( - - ) + shallowRender({ yValues: ['100.0', '75.0', '150.0'], yTicks: ['a', 'b', 'c'] }) ).toMatchSnapshot(); }); + +function shallowRender(props: Partial = {}) { + return shallow(); +} diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap index b0f69f95417..8adfc888cd0 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/BubbleChart-test.tsx.snap @@ -1,148 +1,51 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should display bubbles 1`] = ` - - - - + + - - - + } + transform="translate(33.21428571428571, 70.07936507936509) scale(1)" + /> + + `; exports[`should display bubbles 2`] = ` - - - - + + - - - + } + transform="translate(66.42857142857142, 33.57142857142858) scale(1)" + /> + + `; exports[`should render bubble links 1`] = ` - - - - - - - - - - - -`; - -exports[`should render bubble links 2`] = ` - - - - - - - - - - - -`; - -exports[`should render bubbles with click handlers 1`] = ` - - - + + + - - - + + + `; -exports[`should render bubbles with click handlers 2`] = ` - - - +exports[`should render bubble links 2`] = ` + + + - - - + + + +`; + +exports[`should render bubbles with click handlers 1`] = ` + + + + + +`; + +exports[`should render bubbles with click handlers 2`] = ` + + + + + `; diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap index f52a4ea6cf3..9f2ac323b09 100644 --- a/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/Histogram-test.tsx.snap @@ -1,6 +1,162 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`renders 1`] = ` +exports[`correctly handles yScale() returning undefined 1`] = ` + + + + + + + + 100.0 + + + + a + + + + + + + 75.0 + + + + b + + + + + + + 150.0 + + + + c + + + + + +`; + +exports[`renders correctly: align ticks 1`] = ` + + + + + + + + + + + + + + + +`; + +exports[`renders correctly: default 1`] = ` `; -exports[`renders with yValues 1`] = ` +exports[`renders correctly: with yValues 1`] = ` `; -exports[`renders with yValues and yTicks 1`] = ` +exports[`renders correctly: with yValues and yTicks 1`] = ` `; -exports[`renders with yValues, yTicks and yTooltips 1`] = ` +exports[`renders correctly: with yValues, yTicks and yTooltips 1`] = ` extends React.PureComponent { const result = this.props.onConfirm(this.props.confirmData); if (result) { - return result.then(this.props.onClose, () => {}); + return result.then(this.props.onClose, () => { + /* noop */ + }); } this.props.onClose(); return undefined; diff --git a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx index 92b1444aeb7..04f2b3c8208 100644 --- a/server/sonar-web/src/main/js/components/controls/Dropdown.tsx +++ b/server/sonar-web/src/main/js/components/controls/Dropdown.tsx @@ -119,9 +119,6 @@ interface OverlayProps { placement?: PopupPlacement; } -// TODO use the same styling for -// TODO use the same styling for - export class DropdownOverlay extends React.Component { get placement() { return this.props.placement || PopupPlacement.Bottom; diff --git a/server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx b/server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx index b2d5b1d21dd..5a68bf9d649 100644 --- a/server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx +++ b/server/sonar-web/src/main/js/components/controls/GlobalMessages.tsx @@ -24,7 +24,7 @@ import { colors, sizes, zIndexes } from '../../app/theme'; import { cutLongWords } from '../../helpers/path'; import { ClearButton } from './buttons'; -interface Message { +interface IMessage { id: string; level: 'ERROR' | 'SUCCESS'; message: string; @@ -32,7 +32,7 @@ interface Message { export interface GlobalMessagesProps { closeGlobalMessage: (id: string) => void; - messages: Message[]; + messages: IMessage[]; } export default function GlobalMessages({ closeGlobalMessage, messages }: GlobalMessagesProps) { @@ -60,7 +60,7 @@ const MessagesContainer = styled.div` export class GlobalMessage extends React.PureComponent<{ closeGlobalMessage: (id: string) => void; - message: Message; + message: IMessage; }> { handleClose = () => { this.props.closeGlobalMessage(this.props.message.id); @@ -94,7 +94,7 @@ const appearAnim = keyframes` } `; -const Message = styled.div>` +const Message = styled.div>` position: relative; padding: 0 30px 0 10px; line-height: ${sizes.controlHeight}; @@ -112,7 +112,7 @@ const Message = styled.div>` } `; -const CloseButton = styled(ClearButton)>` +const CloseButton = styled(ClearButton)>` position: absolute; top: calc(${sizes.gridSize} / 4); right: calc(${sizes.gridSize} / 4); diff --git a/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx b/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx index bb9cfcab90f..0e844b113d2 100644 --- a/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx +++ b/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx @@ -31,8 +31,8 @@ interface Props { id?: string; label?: React.ReactNode; name: string; - onBlur: (event: React.FocusEvent) => void; - onChange: (event: React.ChangeEvent) => void; + onBlur: (event: React.FocusEvent) => void; + onChange: (event: React.ChangeEvent) => void; placeholder?: string; touched: boolean | undefined; type?: string; diff --git a/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx b/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx index 2ccf856ce30..0af42bbca83 100644 --- a/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx +++ b/server/sonar-web/src/main/js/components/controls/RadioToggle.tsx @@ -46,7 +46,7 @@ export default class RadioToggle extends React.PureComponent { renderOption = (option: Option) => { const checked = option.value === this.props.value; - const htmlId = this.props.name + '__' + option.value; + const htmlId = `${this.props.name}__${option.value}`; return (
  • { throttledPosition: () => void; @@ -82,8 +84,6 @@ export default class ScreenPositionFixer extends React.Component { }; position = () => { - const edgeMargin = 0.5 * rawSizes.grid; - // eslint-disable-next-line react/no-find-dom-node const node = findDOMNode(this); if (node && node instanceof Element) { @@ -91,17 +91,17 @@ export default class ScreenPositionFixer extends React.Component { const { clientHeight, clientWidth } = document.documentElement; let leftFix = 0; - if (left < edgeMargin) { - leftFix = edgeMargin - left; - } else if (left + width > clientWidth - edgeMargin) { - leftFix = clientWidth - edgeMargin - left - width; + if (left < EDGE_MARGIN) { + leftFix = EDGE_MARGIN - left; + } else if (left + width > clientWidth - EDGE_MARGIN) { + leftFix = clientWidth - EDGE_MARGIN - left - width; } let topFix = 0; - if (top < edgeMargin) { - topFix = edgeMargin - top; - } else if (top + height > clientHeight - edgeMargin) { - topFix = clientHeight - edgeMargin - top - height; + if (top < EDGE_MARGIN) { + topFix = EDGE_MARGIN - top; + } else if (top + height > clientHeight - EDGE_MARGIN) { + topFix = clientHeight - EDGE_MARGIN - top - height; } this.setState({ leftFix, topFix }); diff --git a/server/sonar-web/src/main/js/components/controls/SearchBox.tsx b/server/sonar-web/src/main/js/components/controls/SearchBox.tsx index d03081e26bc..476b8f93b7e 100644 --- a/server/sonar-web/src/main/js/components/controls/SearchBox.tsx +++ b/server/sonar-web/src/main/js/components/controls/SearchBox.tsx @@ -135,7 +135,11 @@ export default class SearchBox extends React.PureComponent {
    + title={ + tooShort && minLength !== undefined + ? translateWithParameters('select2.tooShort', minLength) + : '' + }> { /> )} - {tooShort && ( + {tooShort && minLength !== undefined && ( - {translateWithParameters('select2.tooShort', minLength!)} + {translateWithParameters('select2.tooShort', minLength)} )}
    diff --git a/server/sonar-web/src/main/js/components/controls/Select.tsx b/server/sonar-web/src/main/js/components/controls/Select.tsx index 44657a87991..cc345e12677 100644 --- a/server/sonar-web/src/main/js/components/controls/Select.tsx +++ b/server/sonar-web/src/main/js/components/controls/Select.tsx @@ -18,8 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { - defaultFilterOptions as reactSelectDefaultFilterOptions, +import ReactSelectClass, { ReactAsyncSelectProps, ReactCreatableSelectProps, ReactSelectProps @@ -28,10 +27,6 @@ import { lazyLoadComponent } from '../lazyLoadComponent'; import { ClearButton } from './buttons'; import './Select.css'; -declare module 'react-select' { - export function defaultFilterOptions(...args: any[]): any; -} - const ReactSelectLib = import('react-select'); const ReactSelect = lazyLoadComponent(() => ReactSelectLib); const ReactCreatable = lazyLoadComponent(() => @@ -43,30 +38,22 @@ function renderInput() { return ; } -interface WithInnerRef { - innerRef?: (element: React.Component) => void; +export interface WithInnerRef { + innerRef?: React.Ref>; } export default function Select({ innerRef, ...props }: WithInnerRef & ReactSelectProps) { - // TODO try to define good defaults, if any - // ReactSelect doesn't declare `clearRenderer` prop - const ReactSelectAny = ReactSelect as any; // hide the "x" icon when select is empty const clearable = props.clearable ? Boolean(props.value) : false; return ( - + ); } -export const defaultFilterOptions = reactSelectDefaultFilterOptions; - export function Creatable(props: ReactCreatableSelectProps) { - // ReactSelect doesn't declare `clearRenderer` prop - const ReactCreatableAny = ReactCreatable as any; - return ; + return ; } -// TODO figure out why `ref` prop is incompatible -export function AsyncSelect(props: ReactAsyncSelectProps & { ref?: any }) { +export function AsyncSelect(props: ReactAsyncSelectProps & WithInnerRef) { return ; } diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.css b/server/sonar-web/src/main/js/components/controls/Tooltip.css index d49494f1f77..3b03e423792 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.css +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.css @@ -56,9 +56,6 @@ border-radius: 4px; overflow: hidden; word-break: break-word; -} - -.tooltip-inner { padding: 12px 17px; color: #fff; background-color: #475760; diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx index 802541ad1bc..79facbd7945 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx @@ -334,7 +334,7 @@ export class TooltipInner extends React.Component { } const { classNameSpace = 'tooltip' } = this.props; - const placement = this.getPlacement(); + const currentPlacement = this.getPlacement(); const style = isMeasured(this.state) ? { left: this.state.left + leftFix, @@ -346,7 +346,7 @@ export class TooltipInner extends React.Component { return (
    { className={`${classNameSpace}-arrow`} style={ isMeasured(this.state) - ? this.adjustArrowPosition(placement, { leftFix, topFix }) + ? this.adjustArrowPosition(currentPlacement, { leftFix, topFix }) : undefined } /> diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx new file mode 100644 index 00000000000..2b3bd985de7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/controls/__tests__/Select-test.tsx @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 { shallow } from 'enzyme'; +import * as React from 'react'; +import { ReactAsyncSelectProps, ReactCreatableSelectProps, ReactSelectProps } from 'react-select'; +import Select, { AsyncSelect, Creatable, WithInnerRef } from '../Select'; + +describe('Select', () => { + it('should render correctly', () => { + return new Promise((resolve, reject) => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ clearable: true, value: undefined })).toMatchSnapshot( + 'disable clear button if no value' + ); + + const clearRenderFn = shallowRender().props().clearRenderer; + if (!clearRenderFn) { + reject(); + return; + } + expect(clearRenderFn()).toMatchSnapshot('clear button'); + + resolve(); + }); + }); + + function shallowRender(props: Partial = {}) { + return shallow(