From 2f215846b2e803811048223784b1c0023790da4f Mon Sep 17 00:00:00 2001 From: Pascal Mugnier Date: Mon, 28 May 2018 09:57:06 +0200 Subject: [PATCH] Fix SONAR-10639 (#201) --- .../js/apps/coding-rules/components/Facet.tsx | 7 +- .../component-measures/sidebar/Sidebar.js | 2 +- .../js/apps/issues/sidebar/AssigneeFacet.tsx | 15 +- .../js/apps/issues/sidebar/AuthorFacet.tsx | 17 +- .../js/apps/issues/sidebar/DirectoryFacet.tsx | 24 ++- .../main/js/apps/issues/sidebar/FileFacet.tsx | 17 +- .../js/apps/issues/sidebar/LanguageFacet.tsx | 17 +- .../js/apps/issues/sidebar/ModuleFacet.tsx | 17 +- .../js/apps/issues/sidebar/ProjectFacet.tsx | 17 +- .../apps/issues/sidebar/ResolutionFacet.tsx | 20 ++- .../main/js/apps/issues/sidebar/RuleFacet.tsx | 17 +- .../js/apps/issues/sidebar/SeverityFacet.tsx | 17 +- .../js/apps/issues/sidebar/StatusFacet.tsx | 17 +- .../main/js/apps/issues/sidebar/TagFacet.tsx | 18 ++- .../main/js/apps/issues/sidebar/TypeFacet.tsx | 17 +- .../sidebar/__tests__/AssigneeFacet-test.tsx | 8 +- .../__snapshots__/AssigneeFacet-test.tsx.snap | 45 ++++++ .../main/js/apps/projects/filters/Filter.tsx | 7 +- .../main/js/components/facet/FacetItem.tsx | 53 +++++-- .../facet/__tests__/FacetItem-test.tsx | 2 +- .../__snapshots__/FacetItem-test.tsx.snap | 146 ++++++++++-------- .../src/main/js/components/facet/utils.ts | 23 +++ .../resources/org/sonar/l10n/core.properties | 2 + 23 files changed, 370 insertions(+), 155 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/facet/utils.ts diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx index 7e844278970..f2d93003e5e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx @@ -51,16 +51,18 @@ interface Props extends BasicProps { } export default class Facet extends React.PureComponent { - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { values } = this.props; let newValue; if (this.props.singleSelection) { const value = values.length ? values[0] : undefined; newValue = itemValue === value ? undefined : itemValue; - } else { + } else if (multiple) { newValue = orderBy( values.includes(itemValue) ? without(values, itemValue) : [...values, itemValue] ); + } else { + newValue = values.includes(itemValue) && values.length < 2 ? [] : [itemValue]; } this.props.onChange({ [this.props.property]: newValue }); }; @@ -85,6 +87,7 @@ export default class Facet extends React.PureComponent { name={renderName(value)} onClick={this.handleItemClick} stat={stat && formatMeasure(stat, 'SHORT_INT')} + tooltip={this.props.values.length === 1 && !active} value={value} /> ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js index 184b47b8f5b..4b8edc255c3 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js @@ -85,8 +85,8 @@ export default class Sidebar extends React.PureComponent { /> {groupByDomains(this.props.measures).map(domain => ( { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { + const { assignees } = this.props; if (itemValue === '') { // unassigned this.props.onChange({ assigned: !this.props.assigned, assignees: [] }); - } else { - // defined assignee - const { assignees } = this.props; + } else if (multiple) { const newValue = sortBy( assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue] ); - this.props.onChange({ assigned: true, assignees: newValue }); + this.props.onChange({ assigned: true, [this.property]: newValue }); + } else { + this.props.onChange({ + assigned: true, + [this.property]: assignees.includes(itemValue) && assignees.length < 2 ? [] : [itemValue] + }); } }; @@ -169,6 +173,7 @@ export default class AssigneeFacet extends React.PureComponent { name={this.getAssigneeName(assignee)} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(assignee), this.props.facetMode)} + tooltip={this.props.assignees.length === 1 && !this.isAssigneeActive(assignee)} value={assignee} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx index a09a7e62a42..01e9bc56a9e 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx @@ -43,12 +43,18 @@ export default class AuthorFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { authors } = this.props; - const newValue = sortBy( - authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = sortBy( + authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: authors.includes(itemValue) && authors.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -83,6 +89,7 @@ export default class AuthorFacet extends React.PureComponent { name={author} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(author), this.props.facetMode)} + tooltip={this.props.authors.length === 1 && !this.props.authors.includes(author)} value={author} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx index 32f8fd2bdf5..e26b9343194 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx @@ -46,14 +46,21 @@ export default class DirectoryFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { directories } = this.props; - const newValue = sortBy( - directories.includes(itemValue) - ? without(directories, itemValue) - : [...directories, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = sortBy( + directories.includes(itemValue) + ? without(directories, itemValue) + : [...directories, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: + directories.includes(itemValue) && directories.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -97,6 +104,9 @@ export default class DirectoryFacet extends React.PureComponent { name={this.renderName(directory)} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(directory), this.props.facetMode)} + tooltip={ + this.props.directories.length === 1 && !this.props.directories.includes(directory) + } value={directory} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx index db57e1698e4..a7556e900e3 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx @@ -46,12 +46,18 @@ export default class FileFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { files } = this.props; - const newValue = sortBy( - files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = sortBy( + files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: files.includes(itemValue) && files.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -101,6 +107,7 @@ export default class FileFacet extends React.PureComponent { name={this.renderName(file)} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(file), this.props.facetMode)} + tooltip={this.props.files.length === 1 && !this.props.files.includes(file)} value={file} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx index 3c9d5f0bfab..0ab48821328 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx @@ -45,12 +45,18 @@ export default class LanguageFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { languages } = this.props; - const newValue = sortBy( - languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = sortBy( + languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: languages.includes(itemValue) && languages.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -95,6 +101,7 @@ export default class LanguageFacet extends React.PureComponent { name={this.getLanguageName(language)} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(language), this.props.facetMode)} + tooltip={this.props.languages.length === 1 && !this.props.languages.includes(language)} value={language} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx index 9626862ec69..5fccc6f21c5 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx @@ -45,12 +45,18 @@ export default class ModuleFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { modules } = this.props; - const newValue = sortBy( - modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = sortBy( + modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: modules.includes(itemValue) && modules.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -99,6 +105,7 @@ export default class ModuleFacet extends React.PureComponent { name={this.renderName(module)} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(module), this.props.facetMode)} + tooltip={this.props.modules.length === 1 && !this.props.modules.includes(module)} value={module} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx index a326a2f65e2..69d9fa87f68 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx @@ -51,12 +51,18 @@ export default class ProjectFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { projects } = this.props; - const newValue = sortBy( - projects.includes(itemValue) ? without(projects, itemValue) : [...projects, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = sortBy( + projects.includes(itemValue) ? without(projects, itemValue) : [...projects, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: projects.includes(itemValue) && projects.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -153,6 +159,7 @@ export default class ProjectFacet extends React.PureComponent { name={this.renderName(project)} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(project), this.props.facetMode)} + tooltip={this.props.projects.length === 1 && !this.props.projects.includes(project)} value={project} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx index 911555a6573..5533b3734c0 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx @@ -44,19 +44,24 @@ export default class ResolutionFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { + const { resolutions } = this.props; if (itemValue === '') { // unresolved this.props.onChange({ resolved: !this.props.resolved, resolutions: [] }); - } else { - // defined resolution - const { resolutions } = this.props; + } else if (multiple) { const newValue = orderBy( resolutions.includes(itemValue) ? without(resolutions, itemValue) : [...resolutions, itemValue] ); - this.props.onChange({ resolved: true, resolutions: newValue }); + this.props.onChange({ resolved: true, [this.property]: newValue }); + } else { + this.props.onChange({ + resolved: true, + [this.property]: + resolutions.includes(itemValue) && resolutions.length < 2 ? [] : [itemValue] + }); } }; @@ -95,6 +100,11 @@ export default class ResolutionFacet extends React.PureComponent { name={this.getFacetItemName(resolution)} onClick={this.handleItemClick} stat={formatFacetStat(stat, this.props.facetMode)} + tooltip={ + this.props.resolutions.length === 1 && + resolution !== '' && + !this.props.resolutions.includes(resolution) + } value={resolution} /> ); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx index 920e2002836..f1d8b43fbe1 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx @@ -48,12 +48,18 @@ export default class RuleFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { rules } = this.props; - const newValue = sortBy( - rules.includes(itemValue) ? without(rules, itemValue) : [...rules, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = sortBy( + rules.includes(itemValue) ? without(rules, itemValue) : [...rules, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: rules.includes(itemValue) && rules.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -111,6 +117,7 @@ export default class RuleFacet extends React.PureComponent { name={this.getRuleName(rule)} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(rule), this.props.facetMode)} + tooltip={this.props.rules.length === 1 && !this.props.rules.includes(rule)} value={rule} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx index 9b27208e74c..e68f6d8bc91 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx @@ -44,12 +44,18 @@ export default class SeverityFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { severities } = this.props; - const newValue = orderBy( - severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = orderBy( + severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: severities.includes(itemValue) && severities.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -79,6 +85,7 @@ export default class SeverityFacet extends React.PureComponent { name={} onClick={this.handleItemClick} stat={formatFacetStat(stat, this.props.facetMode)} + tooltip={this.props.severities.length === 1 && !this.props.severities.includes(severity)} value={severity} /> ); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx index ad563640a4f..f34c2695d08 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx @@ -44,12 +44,18 @@ export default class StatusFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { statuses } = this.props; - const newValue = orderBy( - statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = orderBy( + statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: statuses.includes(itemValue) && statuses.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -79,6 +85,7 @@ export default class StatusFacet extends React.PureComponent { name={} onClick={this.handleItemClick} stat={formatFacetStat(stat, this.props.facetMode)} + tooltip={this.props.statuses.length === 1 && !this.props.statuses.includes(status)} value={status} /> ); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx index 7a2fbe66e12..dab0d43d298 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx @@ -50,12 +50,19 @@ export default class TagFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { tags } = this.props; - const newValue = sortBy( - tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const { tags } = this.props; + const newValue = sortBy( + tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: tags.includes(itemValue) && tags.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -114,6 +121,7 @@ export default class TagFacet extends React.PureComponent { name={this.renderTag(tag)} onClick={this.handleItemClick} stat={formatFacetStat(this.getStat(tag), this.props.facetMode)} + tooltip={this.props.tags.length === 1 && !this.props.tags.includes(tag)} value={tag} /> ))} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx index 146a6dbe634..889115e7111 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx @@ -44,12 +44,18 @@ export default class TypeFacet extends React.PureComponent { open: true }; - handleItemClick = (itemValue: string) => { + handleItemClick = (itemValue: string, multiple: boolean) => { const { types } = this.props; - const newValue = orderBy( - types.includes(itemValue) ? without(types, itemValue) : [...types, itemValue] - ); - this.props.onChange({ [this.property]: newValue }); + if (multiple) { + const newValue = orderBy( + types.includes(itemValue) ? without(types, itemValue) : [...types, itemValue] + ); + this.props.onChange({ [this.property]: newValue }); + } else { + this.props.onChange({ + [this.property]: types.includes(itemValue) && types.length < 2 ? [] : [itemValue] + }); + } }; handleHeaderClick = () => { @@ -82,6 +88,7 @@ export default class TypeFacet extends React.PureComponent { } onClick={this.handleItemClick} stat={formatFacetStat(stat, this.props.facetMode)} + tooltip={this.props.types.length === 1 && !this.props.types.includes(type)} value={type} /> ); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx index 13d70175782..7378d918cc1 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx @@ -35,7 +35,7 @@ const renderAssigneeFacet = (props?: Partial) => open={true} organization={undefined} referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }} - stats={{ '': 5, foo: 13, bar: 7 }} + stats={{ '': 5, foo: 13, bar: 7, baz: 6 }} {...props} /> ); @@ -75,10 +75,10 @@ it('should call onChange', () => { expect(onChange).lastCalledWith({ assigned: false, assignees: [] }); itemOnClick('bar'); - expect(onChange).lastCalledWith({ assigned: true, assignees: ['bar', 'foo'] }); + expect(onChange).lastCalledWith({ assigned: true, assignees: ['bar'] }); - itemOnClick('foo'); - expect(onChange).lastCalledWith({ assigned: true, assignees: [] }); + itemOnClick('baz', true); + expect(onChange).lastCalledWith({ assigned: true, assignees: ['baz', 'foo'] }); }); it('should call onToggle', () => { diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap index 1d183049d3d..e28401150f4 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap @@ -20,6 +20,7 @@ exports[`should render 1`] = ` name="unassigned" onClick={[Function]} stat="5" + tooltip={false} value="" /> + + + { ); } - handleClick = (event: React.SyntheticEvent) => { + handleClick = (event: React.MouseEvent) => { event.preventDefault(); event.currentTarget.blur(); @@ -74,14 +74,15 @@ export default class Filter extends React.PureComponent { let urlOption; if (option) { - if (Array.isArray(value)) { + if (Array.isArray(value) && event.ctrlKey) { if (this.isSelected(option)) { urlOption = value.length > 1 ? value.filter(val => val !== option).join(',') : null; } else { urlOption = value.concat(option).join(','); } } else { - urlOption = this.isSelected(option) ? null : option; + urlOption = + this.isSelected(option) && (!Array.isArray(value) || value.length < 2) ? null : option; } this.props.onQueryChange({ [property]: urlOption }); diff --git a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx index 17f9a3c4b16..ccd19527646 100644 --- a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx +++ b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx @@ -19,6 +19,9 @@ */ import * as React from 'react'; import * as classNames from 'classnames'; +import { isOnMac } from './utils'; +import Tooltip from '../controls/Tooltip'; +import { translate } from '../../helpers/l10n'; export interface Props { active?: boolean; @@ -27,8 +30,9 @@ export interface Props { halfWidth?: boolean; loading?: boolean; name: React.ReactNode; - onClick: (x: string) => void; + onClick: (x: string, multiple?: boolean) => void; stat?: React.ReactNode; + tooltip?: boolean; value: string; } @@ -39,10 +43,10 @@ export default class FacetItem extends React.PureComponent { loading: false }; - handleClick = (event: React.SyntheticEvent) => { + handleClick = (event: React.MouseEvent) => { event.preventDefault(); event.currentTarget.blur(); - this.props.onClick(this.props.value); + this.props.onClick(this.props.value, event.ctrlKey || event.metaKey); }; render() { @@ -51,20 +55,37 @@ export default class FacetItem extends React.PureComponent { 'search-navigator-facet-half': this.props.halfWidth }); - return this.props.disabled ? ( - - {this.props.name} - {this.props.stat != null && ( - {this.props.loading ? '' : this.props.stat} - )} - - ) : ( - - {this.props.name} - {this.props.stat != null && ( - {this.props.loading ? '' : this.props.stat} + const overlay = + this.props.tooltip && !this.props.disabled + ? translate( + isOnMac() + ? 'shortcuts.section.global.facets.multiselection.mac' + : 'shortcuts.section.global.facets.multiselection' + ) + : undefined; + + return ( + + {this.props.disabled ? ( + + {this.props.name} + {this.props.stat != null && ( + {this.props.loading ? '' : this.props.stat} + )} + + ) : ( + + {this.props.name} + {this.props.stat != null && ( + {this.props.loading ? '' : this.props.stat} + )} + )} - + ); } } diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx index 95c822c8eb3..e6633a9ff6d 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx +++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx @@ -49,7 +49,7 @@ it('should render half width', () => { it('should call onClick', () => { const onClick = jest.fn(); const wrapper = renderFacetItem({ onClick }); - click(wrapper, { currentTarget: { blur() {}, dataset: { value: 'bar' } } }); + click(wrapper.find('a'), { currentTarget: { blur() {}, dataset: { value: 'bar' } } }); expect(onClick).toHaveBeenCalled(); }); diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap index 3044d7ddbbd..959d2393107 100644 --- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap @@ -1,94 +1,118 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should loading stat 1`] = ` - - - foo - - + + foo + + + `; exports[`should render active 1`] = ` - - - foo - - + + foo + + + `; exports[`should render disabled 1`] = ` - - foo + + foo + - + `; exports[`should render half width 1`] = ` - - - foo - - + + foo + + + `; exports[`should render inactive 1`] = ` - - - foo - - + + foo + + + `; exports[`should render stat 1`] = ` - - - foo - - - 13 - - + + foo + + + 13 + + + `; diff --git a/server/sonar-web/src/main/js/components/facet/utils.ts b/server/sonar-web/src/main/js/components/facet/utils.ts new file mode 100644 index 00000000000..8c55de0abdd --- /dev/null +++ b/server/sonar-web/src/main/js/components/facet/utils.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +export function isOnMac() { + return navigator.userAgent.indexOf('Mac OS X') !== -1; +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 92c53af4b18..b3d6d238765 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -893,6 +893,8 @@ help.section.tutorials=Tutorials shortcuts.section.global=Global shortcuts.section.global.search=quickly open search bar shortcuts.section.global.shortcuts=open this window +shortcuts.section.global.facets.multiselection=Ctrl + click to add to selection +shortcuts.section.global.facets.multiselection.mac=Cmd + click to add to selection shortcuts.section.issues=Issues Page shortcuts.section.issues.navigate_between_issues=navigate between issues -- 2.39.5