From: Pascal Mugnier Date: Tue, 20 Mar 2018 10:56:00 +0000 (+0100) Subject: SONAR-9642 Reduce steps for selecting a custom chart (#6) X-Git-Tag: 7.5~1508 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=82a1cb0b4a6ea692624b290b15080fdf7d1b475c;p=sonarqube.git SONAR-9642 Reduce steps for selecting a custom chart (#6) --- diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css index 5e479d6fc20..11e247b18a0 100644 --- a/server/sonar-web/src/main/js/app/styles/components/menu.css +++ b/server/sonar-web/src/main/js/app/styles/components/menu.css @@ -56,6 +56,10 @@ background-color: var(--barBorderColor); } +.menu > li > a.disabled { + color: #bbb !important; +} + .menu > li > a:hover, .menu > li > a:focus { text-decoration: none; @@ -75,11 +79,20 @@ background-color: var(--barBackgroundColor); } -.menu .menu-vertically-limited { +.menu .menu-vertically-limited, +.menu.menu-vertically-limited { max-height: 300px; overflow-y: auto; } +.menu-vertically-limited.with-top-separator { + border-top: 1px solid #e6e6e6; +} + +.menu-vertically-limited.with-bottom-separator { + border-bottom: 1px solid #e6e6e6; +} + .menu .menu-footer > a > span { border-bottom: 1px solid var(--gray80); color: var(--secondFontColor); diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index 72cadc3c21b..f87994be76e 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -29,6 +29,10 @@ vertical-align: middle; } +.vertical-text-top { + vertical-align: text-top; +} + .nowrap { white-space: nowrap; } diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx index 97b99759784..87359ccaf3f 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx @@ -101,6 +101,7 @@ export default class RuleDetailsMeta extends React.PureComponent { const { canWrite, ruleDetails } = this.props; const { sysTags = [], tags = [] } = ruleDetails; const allTags = [...sysTags, ...tags]; + return (
  • {this.props.canWrite ? ( @@ -111,7 +112,7 @@ export default class RuleDetailsMeta extends React.PureComponent { organization={this.props.organization} setTags={this.props.onTagsChange} sysTags={sysTags} - tags={tags} + tags={allTags} /> } position="bottomleft" diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx index ddd9a5d54ed..38d3520623c 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { without, uniq } from 'lodash'; +import { without, uniq, difference } from 'lodash'; import TagsSelector from '../../../components/tags/TagsSelector'; import { getRuleTags } from '../../../api/rules'; import { BubblePopupPosition } from '../../../components/common/BubblePopup'; @@ -74,6 +74,7 @@ export default class RuleDetailsTagsPopup extends React.PureComponent ); } diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx index 4dde591feb7..bdc74e4d941 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx @@ -85,11 +85,11 @@ export default class MetaTags extends React.PureComponent { right: containerPos.width - eltPos.width }); - handleSetProjectTags = (tags: string[]) => { - setProjectTags({ project: this.props.component.key, tags: tags.join(',') }).then( - () => this.props.onComponentChange({ tags }), - () => {} - ); + handleSetProjectTags = (values: string[]) => { + setProjectTags({ + project: this.props.component.key, + tags: values.join(',') + }).then(() => this.props.onComponentChange({ tags: values }), () => {}); }; render() { diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx index 2fdd9fe1f6e..783688ebf7c 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx +++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { without } from 'lodash'; +import { without, difference } from 'lodash'; import TagsSelector from '../../../components/tags/TagsSelector'; import { BubblePopupPosition } from '../../../components/common/BubblePopup'; import { searchProjectTags } from '../../../api/components'; @@ -71,6 +71,7 @@ export default class MetaTagsSelector extends React.PureComponent }; render() { + const availableTags = difference(this.state.searchResult, this.props.selectedTags); return ( onUnselect={this.onUnselect} position={this.props.position} selectedTags={this.props.selectedTags} - tags={this.state.searchResult} + tags={availableTags} /> ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js index 908b2b09241..43057590919 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js @@ -202,6 +202,7 @@ export default class ProjectActivityGraphs extends React.PureComponent { graph={query.graph} metrics={metrics} metricsTypeFilter={this.getMetricsTypeFilter()} + removeCustomMetric={this.removeCustomMetric} selectedMetrics={this.props.query.customMetrics} updateGraph={this.updateGraph} /> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js index 7b0a4a2a4bb..c9d1c0ec807 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js @@ -28,6 +28,7 @@ import { translate } from '../../../helpers/l10n'; /*:: type Props = { addCustomMetric: string => void, + removeCustomMetric: string => void, graph: string, metrics: Array, metricsTypeFilter: ?Array, @@ -56,10 +57,10 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent { - - {metricsTypeFilter != null && metricsTypeFilter.length > 0 - ? translateWithParameters( - 'project_activity.graphs.custom.type_x_message', - metricsTypeFilter - .map(type => translate('metric.type', type)) - .sort() - .join(', ') - ) - : translate('project_activity.graphs.custom.add_metric_info')} - - - -
    -
    - - -
    -
    - - - ); - } - - render() { - if (this.props.selectedMetrics.length >= 6) { - // Use the class .disabled instead of the property to prevent a bug from - // rc-tooltip : https://github.com/react-component/tooltip/issues/18 - return ( - - - - ); - } - - const buttonComponent = ( - + + ); - - if (this.state.open) { - return [buttonComponent, this.renderModal()]; - } - - return buttonComponent; } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx new file mode 100644 index 00000000000..81864bf8920 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx @@ -0,0 +1,79 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 BubblePopup from '../../../../components/common/BubblePopup'; +import MultiSelect from '../../../../components/common/MultiSelect'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; + +interface Props { + elements: string[]; + filterSelected: (query: string, selectedElements: string[]) => string[]; + metricsTypeFilter: string[]; + onSearch: (query: string) => Promise; + onSelect: (item: string) => void; + onUnselect: (item: string) => void; + popupPosition?: any; + renderLabel: (element: string) => React.ReactNode; + selectedElements: string[]; +} + +export default function AddGraphMetricPopup({ elements, metricsTypeFilter, ...props }: Props) { + let footerNode: React.ReactNode = ''; + + if (props.selectedElements.length >= 6) { + footerNode = ( + + {translate('project_activity.graphs.custom.add_metric_info')} + + ); + } else if (metricsTypeFilter != null && metricsTypeFilter.length > 0) { + footerNode = ( + + {translateWithParameters( + 'project_activity.graphs.custom.type_x_message', + metricsTypeFilter + .map((type: string) => translate('metric.type', type)) + .sort() + .join(', ') + )} + + ); + } + + return ( + + + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js index 5a8ae9aa5a1..ad2e53d848a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -111,7 +111,7 @@ export function generateSeries( metrics /*: Array | { [string]: Metric } */, displayedMetrics /*: Array */ ) /*: Array */ { - if (displayedMetrics.length <= 0) { + if (displayedMetrics.length <= 0 || typeof measuresHistory === 'undefined') { return []; } return sortBy( diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx index 91a68f25fe8..892cab66813 100644 --- a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx +++ b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx @@ -18,17 +18,24 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import * as classNames from 'classnames'; import { difference } from 'lodash'; import MultiSelectOption from './MultiSelectOption'; import SearchBox from '../controls/SearchBox'; +import { translateWithParameters } from '../../helpers/l10n'; interface Props { + allowNewElements?: boolean; + allowSelection?: boolean; elements: string[]; + filterSelected?: (query: string, selectedElements: string[]) => string[]; + footerNode?: React.ReactNode; listSize?: number; onSearch: (query: string) => Promise; onSelect: (item: string) => void; onUnselect: (item: string) => void; placeholder: string; + renderLabel?: (element: string) => React.ReactNode; selectedElements: string[]; validateSearchInput?: (value: string) => string; } @@ -42,7 +49,9 @@ interface State { } interface DefaultProps { + filterSelected: (query: string, selectedElements: string[]) => string[]; listSize: number; + renderLabel: (element: string) => React.ReactNode; validateSearchInput: (value: string) => string; } @@ -54,7 +63,10 @@ export default class MultiSelect extends React.PureComponent { mounted = false; static defaultProps: DefaultProps = { - listSize: 10, + filterSelected: (query: string, selectedElements: string[]) => + selectedElements.filter(elem => elem.includes(query)), + listSize: 0, + renderLabel: (element: string) => element, validateSearchInput: (value: string) => value }; @@ -72,7 +84,7 @@ export default class MultiSelect extends React.PureComponent { componentDidMount() { this.mounted = true; this.onSearchQuery(''); - this.updateSelectedElements(this.props); + this.updateSelectedElements(this.props as PropsWithDefault); this.updateUnselectedElements(this.props as PropsWithDefault); if (this.container) { this.container.addEventListener('keydown', this.handleKeyboard, true); @@ -164,13 +176,13 @@ export default class MultiSelect extends React.PureComponent { onUnselectItem = (item: string) => this.props.onUnselect(item); isNewElement = (elem: string, { selectedElements, elements }: Props) => - elem && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1; + elem.length > 0 && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1; - updateSelectedElements = (props: Props) => { + updateSelectedElements = (props: PropsWithDefault) => { this.setState((state: State) => { if (state.query) { return { - selectedElements: [...props.selectedElements.filter(elem => elem.includes(state.query))] + selectedElements: props.filterSelected(state.query, props.selectedElements) }; } else { return { selectedElements: [...props.selectedElements] }; @@ -180,7 +192,9 @@ export default class MultiSelect extends React.PureComponent { updateUnselectedElements = (props: PropsWithDefault) => { this.setState((state: State) => { - if (props.listSize < state.selectedElements.length) { + if (props.listSize === 0) { + return { unselectedElements: difference(props.elements, props.selectedElements) }; + } else if (props.listSize < state.selectedElements.length) { return { unselectedElements: [] }; } else { return { @@ -239,8 +253,17 @@ export default class MultiSelect extends React.PureComponent { }; render() { + const { allowSelection = true, allowNewElements = true, footerNode = '' } = this.props; + const { renderLabel } = this.props as PropsWithDefault; const { query, activeIdx, selectedElements, unselectedElements } = this.state; const activeElement = this.getAllElements(this.props, this.state)[activeIdx]; + const infiniteList = this.props.listSize === 0; + const listClasses = classNames('menu', { + 'menu-vertically-limited': infiniteList, + 'spacer-top': infiniteList, + 'with-top-separator': infiniteList, + 'with-bottom-separator': Boolean(footerNode) + }); return (
    (this.container = div)}> @@ -254,7 +277,11 @@ export default class MultiSelect extends React.PureComponent { value={query} />
    -