Bladeren bron

SONAR-9225 Make all search bars consistent

tags/7.0-RC1
Stas Vilchik 6 jaren geleden
bovenliggende
commit
6805619766
80 gewijzigde bestanden met toevoegingen van 1016 en 1439 verwijderingen
  1. 5
    14
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
  2. 3
    3
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
  3. 6
    24
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
  4. 12
    7
      server/sonar-web/src/main/js/app/components/search/Search.css
  5. 36
    33
      server/sonar-web/src/main/js/app/components/search/Search.js
  6. 3
    11
      server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
  7. 1
    1
      server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap
  8. 3
    15
      server/sonar-web/src/main/js/app/styles/components/menu.css
  9. 1
    5
      server/sonar-web/src/main/js/app/styles/components/search-navigator.css
  10. 0
    15
      server/sonar-web/src/main/js/app/styles/init/icons.css
  11. 0
    1
      server/sonar-web/src/main/js/app/styles/sonar.css
  12. 4
    4
      server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js
  13. 3
    3
      server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css
  14. 8
    14
      server/sonar-web/src/main/js/apps/background-tasks/components/Search.js
  15. 0
    14
      server/sonar-web/src/main/js/apps/code/code.css
  16. 26
    63
      server/sonar-web/src/main/js/apps/code/components/Search.tsx
  17. 26
    5
      server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js
  18. 15
    2
      server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-query-facet.hbs
  19. 1
    0
      server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js
  20. 22
    2
      server/sonar-web/src/main/js/apps/groups/search-view.js
  21. 11
    2
      server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs
  22. 1
    1
      server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js
  23. 10
    40
      server/sonar-web/src/main/js/apps/marketplace/Search.tsx
  24. 16
    18
      server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js
  25. 8
    6
      server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap
  26. 4
    12
      server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js
  27. 9
    15
      server/sonar-web/src/main/js/apps/permission-templates/components/Template.js
  28. 1
    3
      server/sonar-web/src/main/js/apps/permissions/global/store/actions.js
  29. 1
    5
      server/sonar-web/src/main/js/apps/permissions/project/components/App.js
  30. 25
    71
      server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js
  31. 0
    1
      server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
  32. 0
    3
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
  33. 0
    74
      server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.tsx
  34. 9
    12
      server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx
  35. 0
    58
      server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilter-test.tsx
  36. 1
    8
      server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx
  37. 0
    54
      server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.tsx.snap
  38. 9
    4
      server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap
  39. 1
    13
      server/sonar-web/src/main/js/apps/projects/styles.css
  40. 7
    23
      server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
  41. 2
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
  42. 12
    38
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
  43. 1
    0
      server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js
  44. 0
    87
      server/sonar-web/src/main/js/apps/users/components/UsersSearch.js
  45. 0
    35
      server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js
  46. 0
    82
      server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap
  47. 15
    2
      server/sonar-web/src/main/js/apps/users/search-view.js
  48. 12
    3
      server/sonar-web/src/main/js/apps/users/templates/users-search.hbs
  49. 31
    59
      server/sonar-web/src/main/js/apps/web-api/components/Search.tsx
  50. 2
    8
      server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Search-test.tsx.snap
  51. 0
    4
      server/sonar-web/src/main/js/apps/web-api/styles/web-api.css
  52. 13
    1
      server/sonar-web/src/main/js/components/SelectList/index.js
  53. 13
    4
      server/sonar-web/src/main/js/components/SelectList/templates/list.hbs
  54. 10
    14
      server/sonar-web/src/main/js/components/common/MultiSelect.js
  55. 2
    1
      server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js
  56. 16
    52
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap
  57. 53
    16
      server/sonar-web/src/main/js/components/controls/SearchBox.css
  58. 167
    0
      server/sonar-web/src/main/js/components/controls/SearchBox.tsx
  59. 84
    0
      server/sonar-web/src/main/js/components/controls/__tests__/SearchBox-test.tsx
  60. 30
    0
      server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap
  61. 39
    0
      server/sonar-web/src/main/js/components/icons-components/SearchIcon.tsx
  62. 10
    17
      server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
  63. 4
    9
      server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
  64. 22
    26
      server/sonar-web/src/main/js/components/tags/TagsSelector.js
  65. 6
    7
      server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js
  66. 2
    0
      server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap
  67. 1
    1
      server/sonar-web/src/main/js/components/ui/buttons.tsx
  68. 9
    2
      server/sonar-web/src/main/js/helpers/testUtils.ts
  69. 15
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties
  70. 14
    2
      tests/src/test/java/org/sonarqube/pageobjects/Navigation.java
  71. 61
    0
      tests/src/test/java/org/sonarqube/pageobjects/ProjectCodePage.java
  72. 1
    1
      tests/src/test/java/org/sonarqube/pageobjects/organization/MembersPage.java
  73. 9
    12
      tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipUiTest.java
  74. 26
    26
      tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeletionPageTest.java
  75. 46
    14
      tests/src/test/java/org/sonarqube/tests/sourceCode/ProjectCodeTest.java
  76. 0
    74
      tests/src/test/resources/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html
  77. 0
    30
      tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html
  78. 0
    35
      tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html
  79. 0
    60
      tests/src/test/resources/sourceCode/ProjectCodeTest/search.html
  80. 0
    55
      tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html

+ 5
- 14
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx Bestand weergeven

@@ -29,6 +29,7 @@ import {
} from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
import { getProjectBranchUrl } from '../../../../helpers/urls';
import SearchBox from '../../../../components/controls/SearchBox';
import Tooltip from '../../../../components/controls/Tooltip';

interface Props {
@@ -75,8 +76,7 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
}
};

handleSearchChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
this.setState({ query: event.currentTarget.value, selected: null });
handleSearchChange = (query: string) => this.setState({ query, selected: null });

handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
switch (event.keyCode) {
@@ -84,10 +84,6 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
event.preventDefault();
this.openSelected();
return;
case 27:
event.preventDefault();
this.props.onClose();
return;
case 38:
event.preventDefault();
this.selectPrevious();
@@ -151,17 +147,12 @@ export default class ComponentNavBranchesMenu extends React.PureComponent<Props,
isSelected = (branch: Branch) => branch.name === this.getSelected();

renderSearch = () => (
<div className="search-box menu-search">
<button className="search-box-submit button-clean">
<i className="icon-search-new" />
</button>
<input
<div className="menu-search">
<SearchBox
autoFocus={true}
className="search-box-input"
onChange={this.handleSearchChange}
onKeyDown={this.handleKeyDown}
placeholder={translate('search_verb')}
type="search"
placeholder={translate('branches.search_for_branches')}
value={this.state.query}
/>
</div>

+ 3
- 3
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx Bestand weergeven

@@ -66,13 +66,13 @@ it('selects next & previous', () => {
onClose={jest.fn()}
/>
);
elementKeydown(wrapper.find('input'), 40);
elementKeydown(wrapper.find('SearchBox'), 40);
wrapper.update();
expect(wrapper.state().selected).toBe('foo');
elementKeydown(wrapper.find('input'), 40);
elementKeydown(wrapper.find('SearchBox'), 40);
wrapper.update();
expect(wrapper.state().selected).toBe('foobar');
elementKeydown(wrapper.find('input'), 38);
elementKeydown(wrapper.find('SearchBox'), 38);
wrapper.update();
expect(wrapper.state().selected).toBe('foo');
});

+ 6
- 24
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap Bestand weergeven

@@ -5,22 +5,13 @@ exports[`renders list 1`] = `
className="dropdown-menu"
>
<div
className="search-box menu-search"
className="menu-search"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search-new"
/>
</button>
<input
<SearchBox
autoFocus={true}
className="search-box-input"
onChange={[Function]}
onKeyDown={[Function]}
placeholder="search_verb"
type="search"
placeholder="branches.search_for_branches"
value=""
/>
</div>
@@ -181,22 +172,13 @@ exports[`searches 1`] = `
className="dropdown-menu"
>
<div
className="search-box menu-search"
className="menu-search"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search-new"
/>
</button>
<input
<SearchBox
autoFocus={true}
className="search-box-input"
onChange={[Function]}
onKeyDown={[Function]}
placeholder="search_verb"
type="search"
placeholder="branches.search_for_branches"
value="bar"
/>
</div>

+ 12
- 7
server/sonar-web/src/main/js/app/components/search/Search.css Bestand weergeven

@@ -3,6 +3,12 @@
padding-right: 3px;
}

.navbar-search .search-box,
.navbar-search .search-box-input {
width: 310px;
max-width: none;
}

.navbar-search-input {
vertical-align: middle;
width: 310px;
@@ -13,22 +19,20 @@

.navbar-search-input-hint {
position: absolute;
top: 4px;
right: 30px;
top: 1px;
right: 27px;
line-height: var(--controlHeight);
font-size: var(--smallFontSize);
color: var(--secondFontColor);
}
.navbar-search-input-hint.is-shifted {
z-index: 7501;
top: 32px;
}

.navbar-search-icon {
position: relative;
z-index: var(--aboveNormalZIndex);
vertical-align: middle;
width: 16px;
margin-right: -20px;
background-color: #fff;
color: var(--secondFontColor);
}

@@ -78,9 +82,10 @@
}

.global-navbar-search-dropdown {
top: calc(100% + 3px) !important;
max-height: 80vh;
width: 440px;
padding: 0;
padding: 0 !important;
overflow-y: auto;
overflow-x: hidden;
}

+ 36
- 33
server/sonar-web/src/main/js/app/components/search/Search.js Bestand weergeven

@@ -23,6 +23,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import key from 'keymaster';
import { debounce, keyBy, uniqBy } from 'lodash';
import { FormattedMessage } from 'react-intl';
import SearchResults from './SearchResults';
import SearchResult from './SearchResult';
import { sortQualifiers } from './utils';
@@ -30,6 +31,7 @@ import { sortQualifiers } from './utils';
import RecentHistory from '../../components/RecentHistory';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import ClockIcon from '../../../components/common/ClockIcon';
import SearchBox from '../../../components/controls/SearchBox';
import { getSuggestions } from '../../../api/components';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { scrollToElement } from '../../../helpers/scrolling';
@@ -59,7 +61,7 @@ type State = {
*/

export default class Search extends React.PureComponent {
/*:: input: HTMLElement; */
/*:: input: HTMLInputElement | null; */
/*:: mounted: boolean; */
/*:: node: HTMLElement; */
/*:: nodes: { [string]: HTMLElement };
@@ -92,7 +94,9 @@ export default class Search extends React.PureComponent {
componentDidMount() {
this.mounted = true;
key('s', () => {
this.input.focus();
if (this.input) {
this.input.focus();
}
this.openSearch();
return false;
});
@@ -169,6 +173,12 @@ export default class Search extends React.PureComponent {
return uniqBy([...components, ...recentlyBrowsed], 'key');
};

stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

search = (query /*: string */) => {
if (query.length === 0 || query.length >= 2) {
this.setState({ loading: true });
@@ -191,10 +201,10 @@ export default class Search extends React.PureComponent {
projects: { ...state.projects, ...keyBy(response.projects, 'key') },
results,
selected: list.length > 0 ? list[0] : null,
shortQuery: response.warning === 'short_input'
shortQuery: query.length > 2 && response.warning === 'short_input'
}));
}
});
}, this.stopLoading);
} else {
this.setState({ loading: false });
}
@@ -221,12 +231,11 @@ export default class Search extends React.PureComponent {
selected: moreResults.length > 0 ? moreResults[0].key : state.selected
}));
}
});
}, this.stopLoading);
}
};

handleQueryChange = (event /*: { currentTarget: HTMLInputElement } */) => {
const query = event.currentTarget.value;
handleQueryChange = (query /*: string */) => {
this.setState({ query, shortQuery: query.length === 1 });
this.search(query);
};
@@ -278,10 +287,6 @@ export default class Search extends React.PureComponent {
event.preventDefault();
this.openSelected();
return;
case 27:
event.preventDefault();
this.closeSearch();
return;
case 38:
event.preventDefault();
this.selectPrevious();
@@ -297,10 +302,18 @@ export default class Search extends React.PureComponent {
this.setState({ selected });
};

handleClick = (event /*: Event */) => {
event.stopPropagation();
};

innerRef = (component /*: string */, node /*: HTMLElement */) => {
this.nodes[component] = node;
};

searchInputRef = (node /*: HTMLInputElement | null */) => {
this.input = node;
};

renderResult = (component /*: Component */) => (
<SearchResult
appState={this.props.appState}
@@ -326,30 +339,21 @@ export default class Search extends React.PureComponent {

return (
<li className={dropdownClassName}>
<DeferredSpinner className="navbar-search-icon" loading={this.state.loading}>
<i className="navbar-search-icon icon-search" />
</DeferredSpinner>

<input
autoComplete="off"
className="navbar-search-input js-search-input"
maxLength="30"
name="q"
<DeferredSpinner className="navbar-search-icon" loading={this.state.loading} />

<SearchBox
innerRef={this.searchInputRef}
minLength={2}
onChange={this.handleQueryChange}
onClick={event => event.stopPropagation()}
onClick={this.handleClick}
onFocus={this.openSearch}
onKeyDown={this.handleKeyDown}
ref={node => (this.input = node)}
placeholder={translate('search.placeholder')}
type="search"
value={this.state.query}
/>

{this.state.shortQuery && (
<span
className={classNames('navbar-search-input-hint', {
'is-shifted': this.state.query.length > 5
})}>
<span className={classNames('navbar-search-input-hint')}>
{translateWithParameters('select2.tooShort', 2)}
</span>
)}
@@ -375,12 +379,11 @@ export default class Search extends React.PureComponent {
<ClockIcon className="little-spacer-right" size={12} />
{translate('recently_browsed')}
</div>
<div
dangerouslySetInnerHTML={{
__html: translateWithParameters(
'search.shortcut_hint',
'<span class="shortcut-button shortcut-button-small">s</span>'
)
<FormattedMessage
defaultMessage={translate('search.shortcut_hint')}
id="search.shortcut_hint"
values={{
shortcut: <span className="shortcut-button shortcut-button-small">s</span>
}}
/>
</div>

+ 3
- 11
server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js Bestand weergeven

@@ -38,12 +38,12 @@ function component(key /*: string */, qualifier /*: string */ = 'TRK') {
}

function next(form /*: ShallowWrapper */, expected /*: string */) {
elementKeydown(form.find('input'), 40);
elementKeydown(form.find('SearchBox'), 40);
expect(form.state().selected).toBe(expected);
}

function prev(form /*: ShallowWrapper */, expected /*: string */) {
elementKeydown(form.find('input'), 38);
elementKeydown(form.find('SearchBox'), 38);
expect(form.state().selected).toBe(expected);
}

@@ -83,7 +83,7 @@ it('opens selected on enter', () => {
});
const openSelected = jest.fn();
form.instance().openSelected = openSelected;
elementKeydown(form.find('input'), 13);
elementKeydown(form.find('SearchBox'), 13);
expect(openSelected).toBeCalled();
});

@@ -95,14 +95,6 @@ it('shows warning about short input', () => {
expect(form.find('.navbar-search-input-hint')).toMatchSnapshot();
});

it('closes on escape', () => {
const form = render();
form.instance().openSearch();
expect(form.state().open).toBe(true);
elementKeydown(form.find('input'), 27);
expect(form.state().open).toBe(false);
});

it('closes on click outside', () => {
const form = mount(
<Search appState={{ organizationsEnabled: false }} currentUser={{ isLoggedIn: false }} />

+ 1
- 1
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap Bestand weergeven

@@ -10,7 +10,7 @@ exports[`shows warning about short input 1`] = `

exports[`shows warning about short input 2`] = `
<span
className="navbar-search-input-hint is-shifted"
className="navbar-search-input-hint"
>
select2.tooShort.2
</span>

+ 3
- 15
server/sonar-web/src/main/js/app/styles/components/menu.css Bestand weergeven

@@ -99,22 +99,10 @@
padding: 4px 16px 0;
}

.menu-search .search-box,
.menu-search .search-box-input {
font-size: var(--smallFontSize);
}

.menu-search .search-box-submit {
vertical-align: baseline;
}

.menu-search-full-width {
display: flex;
align-items: center;
}

.menu-search-full-width .search-box-input {
flex-grow: 1;
width: auto;
max-width: none;
min-width: 240px;
}

.menu-search ~ .menu > li > a:hover,

+ 1
- 5
server/sonar-web/src/main/js/app/styles/components/search-navigator.css Bestand weergeven

@@ -534,11 +534,7 @@ a.search-navigator-facet:focus .facet-stat {
}

.search-navigator-facet-query {
padding: 7px 10px 27px;
}

.search-navigator-facet-query input {
width: 100%;
padding: 7px 0 27px;
}

.search-navigator-facet-custom-value {

+ 0
- 15
server/sonar-web/src/main/js/app/styles/init/icons.css Bestand weergeven

@@ -709,21 +709,6 @@ a:hover > .icon-radio {
font-size: var(--bigFontSize);
}

.icon-search:before {
content: '\f002';
font-size: var(--bigFontSize);
}

.icon-search-new {
display: inline-block;
vertical-align: top;
width: 16px;
height: 16px;
background-size: 13px 14px;
background: no-repeat center center;
background-image: url('data:image/svg+xml,%3Csvg%20width%3D%2213%22%20height%3D%2214%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M9%206.5c0-.964-.342-1.788-1.027-2.473C7.288%203.342%206.463%203%205.5%203c-.964%200-1.788.342-2.473%201.027C2.342%204.712%202%205.537%202%206.5c0%20.964.342%201.788%201.027%202.473C3.712%209.658%204.537%2010%205.5%2010c.964%200%201.788-.342%202.473-1.027C8.658%208.288%209%207.463%209%206.5zm4%206.5c0%20.27-.1.505-.297.703-.198.198-.432.297-.703.297-.28%200-.516-.1-.703-.297l-2.68-2.672c-.932.647-1.97.97-3.117.97-.745%200-1.457-.145-2.137-.434-.68-.29-1.265-.68-1.758-1.171-.492-.493-.882-1.08-1.17-1.758C.144%207.957%200%207.245%200%206.5c0-.745.145-1.457.434-2.137.29-.68.68-1.265%201.17-1.758.494-.492%201.08-.882%201.76-1.17C4.043%201.144%204.753%201%205.5%201c.745%200%201.457.145%202.137.434.68.29%201.265.68%201.758%201.17.492.494.882%201.08%201.17%201.76.29.68.435%201.39.435%202.136%200%201.146-.323%202.185-.97%203.117l2.68%202.68c.194.193.29.427.29.703z%22%20fill%3D%22%23777%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E');
}

.icon-edit:before {
content: '\f040';
font-size: var(--mediumFontSize);

+ 0
- 1
server/sonar-web/src/main/js/app/styles/sonar.css Bestand weergeven

@@ -47,7 +47,6 @@
@import './components/panels.css';
@import './components/badges.css';
@import './components/columns.css';
@import './components/search.css';
@import './components/side-tabs.css';
@import './components/boxed-group.css';


+ 4
- 4
server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js Bestand weergeven

@@ -48,19 +48,19 @@ describe('Search', () => {

it('should render search form', () => {
const component = shallow(<Search {...defaultProps} />);
expect(component.find('.js-search').length).toBe(1);
expect(component.find('SearchBox').exists()).toBeTruthy();
});

it('should not render search form', () => {
const component = shallow(<Search {...defaultProps} component={{ id: 'ABCD' }} />);
expect(component.find('.js-search').length).toBe(0);
expect(component.find('SearchBox').exists()).toBeFalsy();
});

it('should search', done => {
const searchSpy = jest.fn();
const component = shallow(<Search {...defaultProps} onFilterUpdate={searchSpy} />);
const searchInput = component.find('.js-search');
change(searchInput, 'some search query');
const searchInput = component.find('SearchBox');
searchInput.prop('onChange')('some search query');
setTimeout(() => {
expect(searchSpy).toBeCalledWith({ query: 'some search query' });
done();

+ 3
- 3
server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css Bestand weergeven

@@ -4,7 +4,7 @@
}

.bt-search-form > li + li {
margin-left: 40px;
margin-left: 16px;
}

.bt-search-form-label {
@@ -15,8 +15,8 @@
padding: 4px 0;
}

.bt-search-form-right {
margin-left: auto !important;
.bt-search-form-large {
flex: 1;
}

.bt-workers-warning-icon {

+ 8
- 14
server/sonar-web/src/main/js/apps/background-tasks/components/Search.js Bestand weergeven

@@ -25,6 +25,7 @@ import TypesFilter from './TypesFilter';
import CurrentsFilter from './CurrentsFilter';
import DateFilter from './DateFilter';
import { DEFAULT_FILTERS } from './../constants';
import SearchBox from '../../../components/controls/SearchBox';
import { translate } from '../../../helpers/l10n';

export default class Search extends React.PureComponent {
@@ -54,9 +55,9 @@ export default class Search extends React.PureComponent {
this.props.onFilterUpdate(date);
}

handleQueryChange(query /*: string */) {
handleQueryChange = (query /*: string */) => {
this.props.onFilterUpdate({ query });
}
};

handleReload(e /*: Object */) {
e.target.blur();
@@ -78,18 +79,11 @@ export default class Search extends React.PureComponent {
}

return (
<li>
<h6 className="bt-search-form-label">
{translate('background_tasks.search_by_task_or_component')}
</h6>

<input
onChange={e => this.handleQueryChange(e.target.value)}
<li className="bt-search-form-large">
<SearchBox
onChange={this.handleQueryChange}
placeholder={translate('background_tasks.search_by_task_or_component')}
value={query}
ref="searchInput"
className="js-search input-medium"
type="search"
placeholder={translate('search_verb')}
/>
</li>
);
@@ -143,7 +137,7 @@ export default class Search extends React.PureComponent {

{this.renderSearchBox()}

<li className="bt-search-form-right nowrap">
<li className="nowrap">
<button className="js-reload" onClick={this.handleReload.bind(this)} disabled={loading}>
{translate('reload')}
</button>{' '}

+ 0
- 14
server/sonar-web/src/main/js/apps/code/code.css Bestand weergeven

@@ -55,20 +55,6 @@
display: none;
}

.code-search .search-box {
padding-right: 10px;
}

.code-search .search-box .note {
vertical-align: middle;
opacity: 0;
transition: opacity 0.3s ease;
}

.code-search .search-box input.touched ~ .note {
opacity: 1;
}

.code-components-header {
position: sticky;
top: 95px;

+ 26
- 63
server/sonar-web/src/main/js/apps/code/components/Search.tsx Bestand weergeven

@@ -20,13 +20,13 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import * as classNames from 'classnames';
import { debounce } from 'lodash';
import Components from './Components';
import { getTree } from '../../../api/components';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { parseError } from '../utils';
import { getProjectUrl } from '../../../helpers/urls';
import { Component } from '../types';
import SearchBox from '../../../components/controls/SearchBox';
import { translate } from '../../../helpers/l10n';

interface Props {
branch?: string;
@@ -43,7 +43,6 @@ interface State {
}

export default class Search extends React.PureComponent<Props, State> {
input: HTMLInputElement;
mounted: boolean;

static contextTypes = {
@@ -55,10 +54,6 @@ export default class Search extends React.PureComponent<Props, State> {
loading: false
};

componentWillMount() {
this.handleSearch = debounce(this.handleSearch, 250);
}

componentDidMount() {
this.mounted = true;
}
@@ -79,10 +74,6 @@ export default class Search extends React.PureComponent<Props, State> {
this.mounted = false;
}

checkInputValue(query: string) {
return this.input.value === query;
}

handleSelectNext() {
const { selectedIndex, results } = this.state;
if (results != null && selectedIndex != null && selectedIndex < results.length - 1) {
@@ -114,27 +105,26 @@ export default class Search extends React.PureComponent<Props, State> {
}
}

handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
switch (e.keyCode) {
handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
switch (event.keyCode) {
case 13:
e.preventDefault();
event.preventDefault();
this.handleSelectCurrent();
break;
case 38:
e.preventDefault();
event.preventDefault();
this.handleSelectPrevious();
break;
case 40:
e.preventDefault();
event.preventDefault();
this.handleSelectNext();
break;
default: // do nothing
}
}
};

handleSearch = (query: string) => {
// first time check if value has changed due to debounce
if (this.mounted && this.checkInputValue(query)) {
if (this.mounted) {
const { branch, component, onError } = this.props;
this.setState({ loading: true });

@@ -143,8 +133,7 @@ export default class Search extends React.PureComponent<Props, State> {

getTree(component.key, { branch, q: query, s: 'qualifier,name', qualifiers })
.then(r => {
// second time check if value has change due to api request
if (this.mounted && this.checkInputValue(query)) {
if (this.mounted) {
this.setState({
results: r.components,
selectedIndex: r.components.length > 0 ? 0 : undefined,
@@ -153,8 +142,7 @@ export default class Search extends React.PureComponent<Props, State> {
}
})
.catch(e => {
// second time check if value has change due to api request
if (this.mounted && this.checkInputValue(query)) {
if (this.mounted) {
this.setState({ loading: false });
parseError(e).then(onError);
}
@@ -162,61 +150,36 @@ export default class Search extends React.PureComponent<Props, State> {
}
};

handleQueryChange(query: string) {
handleQueryChange = (query: string) => {
this.setState({ query });
if (query.length < 3) {
if (query.length === 0) {
this.setState({ results: undefined });
} else {
this.handleSearch(query);
}
}

handleInputChange(event: React.SyntheticEvent<HTMLInputElement>) {
const query = event.currentTarget.value;
this.handleQueryChange(query);
}

handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {
event.preventDefault();
const query = this.input.value;
this.handleQueryChange(query);
}
};

render() {
const { component } = this.props;
const { query, loading, selectedIndex, results } = this.state;
const { loading, selectedIndex, results } = this.state;
const selected = selectedIndex != null && results != null ? results[selectedIndex] : undefined;
const containerClassName = classNames('code-search', {
'code-search-with-results': results != null
});
const inputClassName = classNames('search-box-input', {
touched: query.length > 0 && query.length < 3
});
const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier);

return (
<div id="code-search" className={containerClassName}>
<form className="search-box" onSubmit={this.handleSubmit.bind(this)}>
<button className="search-box-submit button-clean">
<i className="icon-search" />
</button>

<input
ref={node => (this.input = node as HTMLInputElement)}
onKeyDown={this.handleKeyDown.bind(this)}
onChange={this.handleInputChange.bind(this)}
value={query}
className={inputClassName}
type="search"
name="q"
placeholder={translate('search_verb')}
maxLength={100}
autoComplete="off"
/>

{loading && <i className="spinner spacer-left" />}

<span className="note spacer-left">{translateWithParameters('select2.tooShort', 3)}</span>
</form>
<SearchBox
minLength={3}
onChange={this.handleQueryChange}
onKeyDown={this.handleKeyDown}
placeholder={translate(
isPortfolio ? 'code.search_placeholder.portfolio' : 'code.search_placeholder'
)}
value={this.state.query}
/>
{loading && <i className="spinner spacer-left" />}

{results != null && (
<Components

+ 26
- 5
server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js Bestand weergeven

@@ -17,17 +17,20 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { debounce } from 'lodash';
import BaseFacet from './base-facet';
import Template from '../templates/facets/coding-rules-query-facet.hbs';

export default BaseFacet.extend({
template: Template,

events() {
events(...args) {
return {
...BaseFacet.prototype.events.apply(this, arguments),
...BaseFacet.prototype.events.apply(this, args),
'submit form': 'onFormSubmit',
'search input': 'onInputSearch'
'keyup input': 'onKeyUp',
'search input': 'onSearch',
'click .js-reset': 'onResetClick'
};
},

@@ -37,6 +40,8 @@ export default BaseFacet.extend({
const value = query.q;
if (value != null) {
this.$('input').val(value);
this.$('.js-hint').toggleClass('hidden', value.length !== 1);
this.$('.js-reset').toggleClass('hidden', value.length === 0);
}
},

@@ -45,8 +50,24 @@ export default BaseFacet.extend({
this.applyFacet();
},

onInputSearch() {
this.applyFacet();
onKeyUp() {
const q = this.$('input').val();
this.$('.js-hint').toggleClass('hidden', q.length !== 1);
this.$('.js-reset').toggleClass('hidden', q.length === 0);
},

onSearch() {
const q = this.$('input').val();
if (q.length !== 1) {
this.applyFacet();
}
},

onResetClick(e) {
e.preventDefault();
this.$('input')
.val('')
.focus();
},

applyFacet() {

+ 15
- 2
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-query-facet.hbs Bestand weergeven

@@ -1,5 +1,18 @@
<div class="search-navigator-facet-query">
<form>
<input type="search" class="search-navigator-facet-input" name="q" placeholder="{{t 'search_verb'}}">
<form class="search-box">
<input class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_for_rules'}}" maxlength="100">
<svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
<path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
</g>
</svg>
<button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
<svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
</svg>
</button>
<span class="js-hint search-box-note hidden" title="{{tp 'select2.tooShort' 2}}">
{{tp 'select2.tooShort' 2}}
</span>
</form>
</div>

+ 1
- 0
server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js Bestand weergeven

@@ -21,6 +21,7 @@ import React from 'react';
import Helmet from 'react-helmet';
import init from '../init';
import { translate } from '../../../helpers/l10n';
import '../../../components/controls/SearchBox.css';

export default class GroupsAppContainer extends React.PureComponent {
componentDidMount() {

+ 22
- 2
server/sonar-web/src/main/js/apps/groups/search-view.js Bestand weergeven

@@ -24,10 +24,15 @@ import Template from './templates/groups-search.hbs';
export default Marionette.ItemView.extend({
template: Template,

ui: {
reset: '.js-reset'
},

events: {
'submit #groups-search-form': 'onFormSubmit',
'search #groups-search-query': 'debouncedOnKeyUp',
'keyup #groups-search-query': 'debouncedOnKeyUp'
'search #groups-search-query': 'initialOnKeyUp',
'keyup #groups-search-query': 'initialOnKeyUp',
'click .js-reset': 'onResetClick'
},

initialize() {
@@ -44,6 +49,12 @@ export default Marionette.ItemView.extend({
this.debouncedOnKeyUp();
},

initialOnKeyUp() {
const q = this.getQuery();
this.ui.reset.toggleClass('hidden', q.length === 0);
this.debouncedOnKeyUp();
},

onKeyUp() {
const q = this.getQuery();
if (q === this._bufferedValue) {
@@ -62,5 +73,14 @@ export default Marionette.ItemView.extend({

search(q) {
return this.collection.fetch({ reset: true, data: { q } });
},

onResetClick(e) {
e.preventDefault();
e.currentTarget.blur();
this.$('#groups-search-query')
.val('')
.focus();
this.onKeyUp();
}
});

+ 11
- 2
server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs Bestand weergeven

@@ -1,6 +1,15 @@
<div class="panel panel-vertical bordered-bottom spacer-bottom">
<form id="groups-search-form" class="search-box">
<button id="groups-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
<input id="groups-search-query" class="search-box-input" type="search" name="q" placeholder="{{t 'search_verb'}}" maxlength="100">
<input id="groups-search-query" class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_by_name'}}" maxlength="100">
<svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
<path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
</g>
</svg>
<button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
<svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
</svg>
</button>
</form>
</div>

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js Bestand weergeven

@@ -56,7 +56,7 @@ class LanguageFacetFooter extends React.PureComponent {
noResultsText={translate('select2.noMatches')}
onChange={this.handleChange}
options={options}
placeholder={translate('search_verb')}
placeholder={translate('search.search_for_languages')}
searchable={true}
/>
</div>

+ 10
- 40
server/sonar-web/src/main/js/apps/marketplace/Search.tsx Bestand weergeven

@@ -18,9 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { debounce } from 'lodash';
import RadioToggle from '../../components/controls/RadioToggle';
import { Query } from './utils';
import RadioToggle from '../../components/controls/RadioToggle';
import SearchBox from '../../components/controls/SearchBox';
import { translate } from '../../helpers/l10n';

interface Props {
@@ -29,33 +29,13 @@ interface Props {
updateQuery: (newQuery: Partial<Query>) => void;
}

interface State {
search?: string;
}

export default class Search extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { search: props.query.search };
this.updateSearch = debounce(this.updateSearch, 250);
}

componentWillReceiveProps(nextProps: Props) {
if (nextProps.query.search !== this.state.search) {
this.setState({ search: nextProps.query.search });
}
}

handleSearch = (e: React.SyntheticEvent<HTMLInputElement>) => {
const search = e.currentTarget.value;
this.setState({ search });
this.updateSearch(search);
export default class Search extends React.PureComponent<Props> {
handleSearch = (search: string) => {
this.props.updateQuery({ search });
};

handleFilterChange = (filter: string) => this.props.updateQuery({ filter });

updateSearch = (search: string) => this.props.updateQuery({ search });

render() {
const { query, updateCenterActive } = this.props;
const radioOptions = [
@@ -77,21 +57,11 @@ export default class Search extends React.PureComponent<Props, State> {
value={query.filter}
/>
</div>
<div className="search-box display-inline-block text-top">
<button className="search-box-submit button-clean">
<i className="icon-search" />
</button>
<input
onChange={this.handleSearch}
value={this.state.search}
className="search-box-input"
type="search"
name="search"
placeholder={translate('search_verb')}
maxLength={100}
autoComplete="off"
/>
</div>
<SearchBox
onChange={this.handleSearch}
placeholder={translate('marketplace.search')}
value={query.search}
/>
</div>
);
}

+ 16
- 18
server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js Bestand weergeven

@@ -19,7 +19,7 @@
*/
//@flow
import React from 'react';
import UsersSearch from '../../users/components/UsersSearch';
import SearchBox from '../../../components/controls/SearchBox';
import { formatMeasure } from '../../../helpers/measures';
import { translate } from '../../../helpers/l10n';

@@ -30,21 +30,19 @@ type Props = {
};
*/

export default class MembersListHeader extends React.PureComponent {
/*:: props: Props; */

render() {
const { total } = this.props;
return (
<div className="panel panel-vertical bordered-bottom spacer-bottom">
<UsersSearch onSearch={this.props.handleSearch} className="display-inline-block" />
{total != null && (
<span className="pull-right little-spacer-top">
<strong>{formatMeasure(total, 'INT')}</strong>{' '}
{translate('organization.members.members')}
</span>
)}
</div>
);
}
export default function MembersListHeader({ handleSearch, total } /*: Props */) {
return (
<div className="panel panel-vertical bordered-bottom spacer-bottom">
<SearchBox
minLength={2}
onChange={handleSearch}
placeholder={translate('search.search_for_users')}
/>
{total != null && (
<span className="pull-right little-spacer-top">
<strong>{formatMeasure(total, 'INT')}</strong> {translate('organization.members.members')}
</span>
)}
</div>
);
}

+ 8
- 6
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap Bestand weergeven

@@ -4,9 +4,10 @@ exports[`should render with the total 1`] = `
<div
className="panel panel-vertical bordered-bottom spacer-bottom"
>
<UsersSearch
className="display-inline-block"
onSearch={[Function]}
<SearchBox
minLength={2}
onChange={[Function]}
placeholder="search.search_for_users"
/>
<span
className="pull-right little-spacer-top"
@@ -24,9 +25,10 @@ exports[`should render without the total 1`] = `
<div
className="panel panel-vertical bordered-bottom spacer-bottom"
>
<UsersSearch
className="display-inline-block"
onSearch={[Function]}
<SearchBox
minLength={2}
onChange={[Function]}
placeholder="search.search_for_users"
/>
</div>
`;

+ 4
- 12
server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js Bestand weergeven

@@ -19,7 +19,7 @@
*/
//@flow
import React from 'react';
import { debounce, without } from 'lodash';
import { without } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { searchProjectTags } from '../../../api/components';

@@ -42,13 +42,7 @@ const LIST_SIZE = 10;

export default class MetaTagsSelector extends React.PureComponent {
/*:: props: Props; */
/*:: state: State; */

constructor(props /*: Props */) {
super(props);
this.state = { searchResult: [] };
this.onSearch = debounce(this.onSearch, 250);
}
state /*: State */ = { searchResult: [] };

componentDidMount() {
this.onSearch('');
@@ -56,12 +50,10 @@ export default class MetaTagsSelector extends React.PureComponent {

onSearch = (query /*: string */) => {
searchProjectTags({
q: query || '',
q: query,
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100)
}).then(result => {
this.setState({
searchResult: result.tags
});
this.setState({ searchResult: result.tags });
});
};


+ 9
- 15
server/sonar-web/src/main/js/apps/permission-templates/components/Template.js Bestand weergeven

@@ -37,18 +37,14 @@ export default class Template extends React.PureComponent {
topQualifiers: PropTypes.array.isRequired
};

constructor(props) {
super(props);
this.state = {
loading: false,
users: [],
groups: [],
query: '',
filter: 'all',
selectedPermission: null
};
this.requestHoldersDebounced = debounce(this.requestHolders, 250);
}
state = {
loading: false,
users: [],
groups: [],
query: '',
filter: 'all',
selectedPermission: null
};

componentDidMount() {
this.mounted = true;
@@ -140,9 +136,7 @@ export default class Template extends React.PureComponent {

handleSearch = query => {
this.setState({ query });
if (query.length === 0 || query.length > 2) {
this.requestHoldersDebounced(query);
}
this.requestHolders(query);
};

handleFilter = filter => {

+ 1
- 3
server/sonar-web/src/main/js/apps/permissions/global/store/actions.js Bestand weergeven

@@ -87,9 +87,7 @@ export const updateQuery = (query /*: string */ = '', organization /*: ?string *
dispatch /*: Dispatch */
) => {
dispatch({ type: UPDATE_QUERY, query });
if (query.length === 0 || query.length > 2) {
dispatch(loadHolders(organization));
}
dispatch(loadHolders(organization));
};

export const updateFilter = (filter /*: string */, organization /*: ?string */) => (

+ 1
- 5
server/sonar-web/src/main/js/apps/permissions/project/components/App.js Bestand weergeven

@@ -151,11 +151,7 @@ export default class App extends React.PureComponent {

handleQueryChange = (query /*: string */) => {
if (this.mounted) {
this.setState({ query }, () => {
if (query.length === 0 || query.length > 2) {
this.loadHolders();
}
});
this.setState({ query }, this.loadHolders);
}
};


+ 25
- 71
server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js Bestand weergeven

@@ -20,79 +20,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import RadioToggle from '../../../../components/controls/RadioToggle';
import SearchBox from '../../../../components/controls/SearchBox';
import { translate, translateWithParameters } from '../../../../helpers/l10n';

export default class SearchForm extends React.PureComponent {
static propTypes = {
query: PropTypes.string,
filter: PropTypes.oneOf(['all', 'users', 'groups']),
onSearch: PropTypes.func,
onFilter: PropTypes.func
};

componentWillMount() {
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSearch = this.handleSearch.bind(this);
}

handleSubmit(e) {
e.preventDefault();
this.handleSearch();
}

handleSearch() {
const { value } = this.refs.searchInput;
this.props.onSearch(value);
}

handleFilter(filter) {
this.props.onFilter(filter);
}

render() {
const { query, filter } = this.props;

const filterOptions = [
{ value: 'all', label: translate('all') },
{ value: 'users', label: translate('users.page') },
{ value: 'groups', label: translate('user_groups.page') }
];

return (
<div>
<RadioToggle
value={filter}
options={filterOptions}
name="users-or-groups"
onCheck={this.handleFilter.bind(this)}
export default function SearchForm(props) {
const filterOptions = [
{ value: 'all', label: translate('all') },
{ value: 'users', label: translate('users.page') },
{ value: 'groups', label: translate('user_groups.page') }
];

return (
<div className="diplay-flex-row">
<RadioToggle
name="users-or-groups"
onCheck={props.onFilter}
options={filterOptions}
value={props.filter}
/>

<div className="flex-1 spacer-left">
<SearchBox
minLength={3}
onChange={props.onSearch}
placeholder={translate('search.search_for_users_or_groups')}
value={props.query}
/>

<form
className="search-box display-inline-block text-middle big-spacer-left"
onSubmit={this.handleSubmit}>
<button className="search-box-submit button-clean">
<i className="icon-search" />
</button>
<input
ref="searchInput"
value={query}
className="search-box-input"
style={{ width: 100 }}
type="search"
placeholder={translate('search_verb')}
onChange={this.handleSearch.bind(this)}
/>
{query.length > 0 &&
query.length < 3 && (
<div className="search-box-input-note tooltip bottom fade in">
<div className="tooltip-inner">
{translateWithParameters('select2.tooShort', 3)}
</div>
<div className="tooltip-arrow" style={{ left: 23 }} />
</div>
)}
</form>
</div>
);
}
</div>
);
}

+ 0
- 1
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx Bestand weergeven

@@ -79,7 +79,6 @@ export default function PageHeader(props: Props) {
)}

<SearchFilterContainer
className="projects-topbar-item projects-topbar-item-search"
isFavorite={props.isFavorite}
organization={props.organization}
query={props.query}

+ 0
- 3
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap Bestand weergeven

@@ -17,7 +17,6 @@ exports[`should render correctly 1`] = `
view="overall"
/>
<SearchFilterContainer
className="projects-topbar-item projects-topbar-item-search"
query={
Object {
"search": "test",
@@ -57,7 +56,6 @@ exports[`should render correctly while loading 1`] = `
view="overall"
/>
<SearchFilterContainer
className="projects-topbar-item projects-topbar-item-search"
query={
Object {
"search": "test",
@@ -110,7 +108,6 @@ exports[`should render disabled sorting options for visualizations 1`] = `
</div>
</Tooltip>
<SearchFilterContainer
className="projects-topbar-item projects-topbar-item-search"
query={
Object {
"search": "test",

+ 0
- 74
server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.tsx Bestand weergeven

@@ -1,74 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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 { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
className?: string;
handleSearch: (userString?: string) => void;
query: { search?: string | undefined };
}

interface State {
userQuery?: string;
}

export default class SearchFilter extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { userQuery: props.query.search };
}

componentWillReceiveProps(nextProps: Props) {
if (
this.props.query.search === this.state.userQuery &&
nextProps.query.search !== this.props.query.search
) {
this.setState({ userQuery: nextProps.query.search || '' });
}
}

handleQueryChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const { value } = event.currentTarget;
this.setState({ userQuery: value });
if (!value || value.length >= 2) {
this.props.handleSearch(value);
}
};

render() {
const { userQuery } = this.state;
const shortQuery = userQuery != null && userQuery.length === 1;
return (
<div className={this.props.className}>
<input
type="search"
value={userQuery || ''}
placeholder={translate('projects.search')}
onChange={this.handleQueryChange}
autoComplete="off"
/>
{shortQuery && (
<span className="note spacer-left">{translateWithParameters('select2.tooShort', 2)}</span>
)}
</div>
);
}
}

+ 9
- 12
server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx Bestand weergeven

@@ -19,9 +19,9 @@
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { debounce } from 'lodash';
import { getFilterUrl } from './utils';
import SearchFilter from './SearchFilter';
import SearchBox from '../../../components/controls/SearchBox';
import { translate } from '../../../helpers/l10n';

interface Props {
className?: string;
@@ -35,11 +35,6 @@ export default class SearchFilterContainer extends React.PureComponent<Props> {
router: PropTypes.object.isRequired
};

constructor(props: Props) {
super(props);
this.handleSearch = debounce(this.handleSearch, 250);
}

handleSearch = (userQuery?: string) => {
const path = getFilterUrl(this.props, { search: userQuery });
this.context.router.push(path);
@@ -47,11 +42,13 @@ export default class SearchFilterContainer extends React.PureComponent<Props> {

render() {
return (
<SearchFilter
className={this.props.className}
query={this.props.query}
handleSearch={this.handleSearch}
/>
<div className="projects-topbar-item projects-topbar-item-search">
<SearchBox
minLength={2}
onChange={this.handleSearch}
placeholder={translate('projects.search')}
/>
</div>
);
}
}

+ 0
- 58
server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilter-test.tsx Bestand weergeven

@@ -1,58 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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 SearchFilter from '../SearchFilter';
import { change } from '../../../../helpers/testUtils';

it('should render correctly without any search query', () => {
const wrapper = shallow(<SearchFilter handleSearch={jest.fn()} query={{}} />);
expect(wrapper).toMatchSnapshot();
});

it('should render with a search query', () => {
const wrapper = shallow(<SearchFilter handleSearch={jest.fn()} query={{ search: 'foo' }} />);
expect(wrapper).toMatchSnapshot();
});

it('should display a help message when there is less than 2 characters', () => {
const wrapper = shallow(<SearchFilter handleSearch={jest.fn()} query={{ search: 'a' }} />);
expect(wrapper).toMatchSnapshot();
wrapper.setState({ userQuery: 'foo' });
expect(wrapper).toMatchSnapshot();
});

it('searches', () => {
const handleSearch = jest.fn();
const wrapper = shallow(<SearchFilter handleSearch={handleSearch} query={{}} />);

change(wrapper.find('input'), 'a');
expect(handleSearch).not.toBeCalled();

change(wrapper.find('input'), 'abc');
expect(handleSearch).toBeCalledWith('abc');
});

it('updates state to new props', () => {
const wrapper = shallow(<SearchFilter handleSearch={jest.fn()} query={{ search: 'abc' }} />);
expect(wrapper.state()).toEqual({ userQuery: 'abc' });
wrapper.setProps({ query: { search: 'def' } });
expect(wrapper.state()).toEqual({ userQuery: 'def' });
});

+ 1
- 8
server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx Bestand weergeven

@@ -21,17 +21,10 @@ import * as React from 'react';
import { shallow } from 'enzyme';
import SearchFilterContainer from '../SearchFilterContainer';

// mocking lodash, because mocking timers is now working for some reason :'(
jest.mock('lodash', () => {
const lodash = require.requireActual('lodash');
lodash.debounce = (fn: Function) => (...args: any[]) => fn(args);
return lodash;
});

it('searches', () => {
const push = jest.fn();
const wrapper = shallow(<SearchFilterContainer query={{}} />, { context: { router: { push } } });
expect(wrapper).toMatchSnapshot();
wrapper.prop('handleSearch')('foo');
wrapper.find('SearchBox').prop<Function>('onChange')('foo');
expect(push).toBeCalledWith({ pathname: '/projects', query: { search: 'foo' } });
});

+ 0
- 54
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.tsx.snap Bestand weergeven

@@ -1,54 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display a help message when there is less than 2 characters 1`] = `
<div>
<input
autoComplete="off"
onChange={[Function]}
placeholder="projects.search"
type="search"
value="a"
/>
<span
className="note spacer-left"
>
select2.tooShort.2
</span>
</div>
`;

exports[`should display a help message when there is less than 2 characters 2`] = `
<div>
<input
autoComplete="off"
onChange={[Function]}
placeholder="projects.search"
type="search"
value="foo"
/>
</div>
`;

exports[`should render correctly without any search query 1`] = `
<div>
<input
autoComplete="off"
onChange={[Function]}
placeholder="projects.search"
type="search"
value=""
/>
</div>
`;

exports[`should render with a search query 1`] = `
<div>
<input
autoComplete="off"
onChange={[Function]}
placeholder="projects.search"
type="search"
value="foo"
/>
</div>
`;

+ 9
- 4
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap Bestand weergeven

@@ -1,8 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`searches 1`] = `
<SearchFilter
handleSearch={[Function]}
query={Object {}}
/>
<div
className="projects-topbar-item projects-topbar-item-search"
>
<SearchBox
minLength={2}
onChange={[Function]}
placeholder="projects.search"
/>
</div>
`;

+ 1
- 13
server/sonar-web/src/main/js/apps/projects/styles.css Bestand weergeven

@@ -41,19 +41,7 @@
.projects-topbar-item-search {
position: relative;
flex: 1;
}

.projects-topbar-item-search input {
width: 100%;
max-width: 300px;
}

.projects-topbar-item-search .note {
position: absolute;
top: 1px;
left: 80px;
line-height: var(--controlHeight);
pointer-events: none;
height: var(--controlHeight);
}

.projects-list .page-actions {

+ 7
- 23
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx Bestand weergeven

@@ -29,6 +29,7 @@ import QualifierIcon from '../../components/shared/QualifierIcon';
import Tooltip from '../../components/controls/Tooltip';
import DateInput from '../../components/controls/DateInput';
import Select from '../../components/controls/Select';
import SearchBox from '../../components/controls/SearchBox';

export interface Props {
analyzedBefore?: string;
@@ -60,16 +61,6 @@ export default class Search extends React.PureComponent<Props, State> {
mounted: boolean;
state: State = { bulkApplyTemplateModal: false, deleteModal: false };

onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
event.preventDefault();
this.search();
};

search = (event?: React.SyntheticEvent<HTMLInputElement>) => {
const q = event ? event.currentTarget.value : this.input.value;
this.props.onSearch(q);
};

getQualifierOptions = () => {
const options = this.props.topLevelQualifiers.map(q => ({
label: translate('qualifiers', q),
@@ -206,19 +197,12 @@ export default class Search extends React.PureComponent<Props, State> {
{this.renderDateFilter()}
{this.renderTypeFilter()}
<td className="text-middle">
<form onSubmit={this.onSubmit} className="search-box">
<button className="search-box-submit button-clean">
<i className="icon-search" />
</button>
<input
onChange={this.search}
value={this.props.query}
ref={node => (this.input = node!)}
className="search-box-input input-medium"
type="search"
placeholder={translate('search_verb')}
/>
</form>
<SearchBox
minLength={3}
onChange={this.props.onSearch}
placeholder={translate('search.search_by_name_or_key')}
value={this.props.query}
/>
</td>
<td className="thin nowrap text-middle">
<button

+ 2
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx Bestand weergeven

@@ -20,7 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Search, { Props } from '../Search';
import { change, click } from '../../../helpers/testUtils';
import { click } from '../../../helpers/testUtils';

const organization = { key: 'org', name: 'org', projectVisibility: 'public' };

@@ -67,7 +67,7 @@ it('updates analysis date', () => {
it('searches', () => {
const onSearch = jest.fn();
const wrapper = shallowRender({ onSearch });
change(wrapper.find('input[type="search"]'), 'foo');
wrapper.find('SearchBox').prop<Function>('onChange')('foo');
expect(onSearch).toBeCalledWith('foo');
});


+ 12
- 38
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap Bestand weergeven

@@ -120,25 +120,12 @@ exports[`render qualifiers filter 1`] = `
<td
className="text-middle"
>
<form
className="search-box"
onSubmit={[Function]}
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search"
/>
</button>
<input
className="search-box-input input-medium"
onChange={[Function]}
placeholder="search_verb"
type="search"
value=""
/>
</form>
<SearchBox
minLength={3}
onChange={[Function]}
placeholder="search.search_by_name_or_key"
value=""
/>
</td>
<td
className="thin nowrap text-middle"
@@ -223,25 +210,12 @@ exports[`renders 1`] = `
<td
className="text-middle"
>
<form
className="search-box"
onSubmit={[Function]}
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search"
/>
</button>
<input
className="search-box-input input-medium"
onChange={[Function]}
placeholder="search_verb"
type="search"
value=""
/>
</form>
<SearchBox
minLength={3}
onChange={[Function]}
placeholder="search.search_by_name_or_key"
value=""
/>
</td>
<td
className="thin nowrap text-middle"

+ 1
- 0
server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js Bestand weergeven

@@ -26,6 +26,7 @@ import { getCurrentUser } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
// import styles to have the `.button-icon` styles
import '../../../components/ui/buttons.css';
import '../../../components/controls/SearchBox.css';

class UsersAppContainer extends React.PureComponent {
static propTypes = {

+ 0
- 87
server/sonar-web/src/main/js/apps/users/components/UsersSearch.js Bestand weergeven

@@ -1,87 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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.
*/
//@flow
import React from 'react';
import { debounce } from 'lodash';
import classNames from 'classnames';
import { translate, translateWithParameters } from '../../../helpers/l10n';

/*::
type Props = {
onSearch: (query?: string) => void,
className?: string
};
*/

/*::
type State = {
query?: string
};
*/

export default class UsersSearch extends React.PureComponent {
/*:: props: Props; */
/*:: state: State; */

constructor(props /*: Props */) {
super(props);
this.state = {
query: ''
};
this.handleSearch = debounce(this.handleSearch, 250);
}

handleSearch = (query /*: string */) => {
this.props.onSearch(query);
};

handleInputChange = ({ target } /*: { target: HTMLInputElement } */) => {
this.setState({ query: target.value });
if (!target.value || target.value.length >= 2) {
this.handleSearch(target.value);
}
};

render() {
const { query } = this.state;
const searchBoxClass = classNames('search-box', this.props.className);
const inputClassName = classNames('search-box-input', {
touched: query != null && query.length === 1
});
return (
<div className={searchBoxClass}>
<button className="search-box-submit button-clean">
<i className="icon-search" />
</button>
<input
type="search"
value={query}
className={inputClassName}
placeholder={translate('search_verb')}
onChange={this.handleInputChange}
autoComplete="off"
/>
<span className="note spacer-left text-middle">
{translateWithParameters('select2.tooShort', 2)}
</span>
</div>
);
}
}

+ 0
- 35
server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js Bestand weergeven

@@ -1,35 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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 React from 'react';
import { shallow } from 'enzyme';
import UsersSearch from '../UsersSearch';

it('should render correctly', () => {
const wrapper = shallow(<UsersSearch onSearch={jest.fn()} className="test" />);
expect(wrapper).toMatchSnapshot();
wrapper.setState({ query: 'foo' });
expect(wrapper).toMatchSnapshot();
});

it('should display a help message when there is less than 2 characters', () => {
const wrapper = shallow(<UsersSearch onSearch={jest.fn()} />);
wrapper.setState({ query: 'f' });
expect(wrapper).toMatchSnapshot();
});

+ 0
- 82
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap Bestand weergeven

@@ -1,82 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display a help message when there is less than 2 characters 1`] = `
<div
className="search-box"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search"
/>
</button>
<input
autoComplete="off"
className="search-box-input touched"
onChange={[Function]}
placeholder="search_verb"
type="search"
value="f"
/>
<span
className="note spacer-left text-middle"
>
select2.tooShort.2
</span>
</div>
`;

exports[`should render correctly 1`] = `
<div
className="search-box test"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search"
/>
</button>
<input
autoComplete="off"
className="search-box-input"
onChange={[Function]}
placeholder="search_verb"
type="search"
value=""
/>
<span
className="note spacer-left text-middle"
>
select2.tooShort.2
</span>
</div>
`;

exports[`should render correctly 2`] = `
<div
className="search-box test"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search"
/>
</button>
<input
autoComplete="off"
className="search-box-input"
onChange={[Function]}
placeholder="search_verb"
type="search"
value="foo"
/>
<span
className="note spacer-left text-middle"
>
select2.tooShort.2
</span>
</div>
`;

+ 15
- 2
server/sonar-web/src/main/js/apps/users/search-view.js Bestand weergeven

@@ -25,13 +25,15 @@ export default Marionette.ItemView.extend({
template: Template,

ui: {
hint: '.js-hint'
hint: '.js-hint',
reset: '.js-reset'
},

events: {
'submit #users-search-form': 'onFormSubmit',
'search #users-search-query': 'initialOnKeyUp',
'keyup #users-search-query': 'initialOnKeyUp'
'keyup #users-search-query': 'initialOnKeyUp',
'click .js-reset': 'onResetClick'
},

initialize() {
@@ -51,6 +53,7 @@ export default Marionette.ItemView.extend({
initialOnKeyUp() {
const q = this.getQuery();
this.ui.hint.toggleClass('hidden', q.length !== 1);
this.ui.reset.toggleClass('hidden', q.length === 0);
this.debouncedOnKeyUp();
},

@@ -64,6 +67,7 @@ export default Marionette.ItemView.extend({
this.searchRequest.abort();
}
this.ui.hint.toggleClass('hidden', q.length !== 1);
this.ui.reset.toggleClass('hidden', q.length === 0);
if (q.length !== 1) {
this.searchRequest = this.search(q);
}
@@ -75,5 +79,14 @@ export default Marionette.ItemView.extend({

search(q) {
return this.collection.fetch({ reset: true, data: { q } });
},

onResetClick(e) {
e.preventDefault();
e.currentTarget.blur();
this.$('#users-search-query')
.val('')
.focus();
this.onKeyUp();
}
});

+ 12
- 3
server/sonar-web/src/main/js/apps/users/templates/users-search.hbs Bestand weergeven

@@ -1,8 +1,17 @@
<div class="panel panel-vertical bordered-bottom spacer-bottom">
<form id="users-search-form" class="search-box">
<button id="users-search-submit" class="search-box-submit button-clean"><i class="icon-search"></i></button>
<input id="users-search-query" class="search-box-input" type="search" name="q" placeholder="{{t 'search_verb'}}" maxlength="100">
<span class="js-hint note spacer-left text-middle hidden">
<input id="users-search-query" class="search-box-input" type="text" name="q" placeholder="{{t 'search.search_by_login_or_name'}}" maxlength="100">
<svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
<path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
</g>
</svg>
<button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
<svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
</svg>
</button>
<span class="js-hint search-box-note hidden" title="{{tp 'select2.tooShort' 2}}">
{{tp 'select2.tooShort' 2}}
</span>
</form>

+ 31
- 59
server/sonar-web/src/main/js/apps/web-api/components/Search.tsx Bestand weergeven

@@ -18,11 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { debounce } from 'lodash';
import Checkbox from '../../../components/controls/Checkbox';
import HelpIcon from '../../../components/icons-components/HelpIcon';
import Tooltip from '../../../components/controls/Tooltip';
import { translate } from '../../../helpers/l10n';
import SearchBox from '../../../components/controls/SearchBox';

interface Props {
showDeprecated: boolean;
@@ -32,66 +32,38 @@ interface Props {
onToggleDeprecated: () => void;
}

interface State {
query: string;
}

export default class Search extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { query: '' };
this.actuallySearch = debounce(this.actuallySearch, 250);
}

handleSearch = (e: React.SyntheticEvent<HTMLInputElement>) => {
this.setState({ query: e.currentTarget.value });
this.actuallySearch();
};

actuallySearch = () => this.props.onSearch(this.state.query);
export default function Search(props: Props) {
const { showInternal, showDeprecated, onToggleInternal, onToggleDeprecated } = props;

render() {
const { showInternal, showDeprecated, onToggleInternal, onToggleDeprecated } = this.props;

return (
<div className="web-api-search">
<div>
<i className="icon-search" />
<input
className="spacer-left input-large"
type="search"
value={this.state.query}
placeholder={translate('search_verb')}
onChange={this.handleSearch}
/>
</div>
return (
<div className="web-api-search">
<div>
<SearchBox onChange={props.onSearch} placeholder={translate('api_documentation.search')} />
</div>

<div className="big-spacer-top">
<Checkbox checked={showInternal} onCheck={onToggleInternal}>
<span className="little-spacer-left">
{translate('api_documentation.show_internal')}
</span>
</Checkbox>
<Tooltip overlay={translate('api_documentation.internal_tooltip')} placement="right">
<span>
<HelpIcon className="spacer-left text-info" />
</span>
</Tooltip>
</div>
<div className="big-spacer-top">
<Checkbox checked={showInternal} onCheck={onToggleInternal}>
<span className="little-spacer-left">{translate('api_documentation.show_internal')}</span>
</Checkbox>
<Tooltip overlay={translate('api_documentation.internal_tooltip')} placement="right">
<span>
<HelpIcon className="spacer-left text-info" />
</span>
</Tooltip>
</div>

<div className="spacer-top">
<Checkbox checked={showDeprecated} onCheck={onToggleDeprecated}>
<span className="little-spacer-left">
{translate('api_documentation.show_deprecated')}
</span>
</Checkbox>
<Tooltip overlay={translate('api_documentation.deprecation_tooltip')} placement="right">
<span>
<HelpIcon className="spacer-left text-info" />
</span>
</Tooltip>
</div>
<div className="spacer-top">
<Checkbox checked={showDeprecated} onCheck={onToggleDeprecated}>
<span className="little-spacer-left">
{translate('api_documentation.show_deprecated')}
</span>
</Checkbox>
<Tooltip overlay={translate('api_documentation.deprecation_tooltip')} placement="right">
<span>
<HelpIcon className="spacer-left text-info" />
</span>
</Tooltip>
</div>
);
}
</div>
);
}

+ 2
- 8
server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Search-test.tsx.snap Bestand weergeven

@@ -5,15 +5,9 @@ exports[`should render correctly 1`] = `
className="web-api-search"
>
<div>
<i
className="icon-search"
/>
<input
className="spacer-left input-large"
<SearchBox
onChange={[Function]}
placeholder="search_verb"
type="search"
value=""
placeholder="api_documentation.search"
/>
</div>
<div

+ 0
- 4
server/sonar-web/src/main/js/apps/web-api/styles/web-api.css Bestand weergeven

@@ -9,10 +9,6 @@
white-space: nowrap;
}

.web-api-search .icon-search {
color: var(--gray80);
}

.web-api-domain-header,
.web-api-action-header {
display: flex;

+ 13
- 1
server/sonar-web/src/main/js/components/SelectList/index.js Bestand weergeven

@@ -25,6 +25,7 @@ import { translate } from '../../helpers/l10n';
import ItemTemplate from './templates/item.hbs';
import ListTemplate from './templates/list.hbs';
import './styles.css';
import '../controls/SearchBox.css';

let showError = null;

@@ -160,7 +161,8 @@ const SelectListView = Backbone.View.extend({
events: {
'click .select-list-control-button[name=selected]': 'showSelected',
'click .select-list-control-button[name=deselected]': 'showDeselected',
'click .select-list-control-button[name=all]': 'showAll'
'click .select-list-control-button[name=all]': 'showAll',
'click .js-reset': 'onResetClick'
},

initialize(options) {
@@ -331,6 +333,7 @@ const SelectListView = Backbone.View.extend({

this.$('.select-list-check-control').toggleClass('disabled', hasQuery);
this.$('.select-list-search-control').toggleClass('disabled', !hasQuery);
this.$('.js-reset').toggleClass('hidden', !hasQuery);

if (hasQuery) {
this.showFetchSpinner();
@@ -352,6 +355,15 @@ const SelectListView = Backbone.View.extend({
}
},

onResetClick(e) {
e.preventDefault();
e.currentTarget.blur();
this.$('.select-list-search-control input')
.val('')
.focus()
.trigger('search');
},

searchByQuery(query) {
this.$('.select-list-search-control input').val(query);
this.search();

+ 13
- 4
server/sonar-web/src/main/js/components/SelectList/templates/list.hbs Bestand weergeven

@@ -4,10 +4,19 @@
<a class="select-list-control-button" name="selected">{{this.selected}}</a><a class="select-list-control-button" name="deselected">{{this.deselected}}</a><a class="select-list-control-button" name="all">{{this.all}}</a>
</div>
<div class="select-list-search-control">
<form class="search-box">
<span class="search-box-submit button-clean"><i class="icon-search"></i></span>
<input class="search-box-input" type="search" name="q" placeholder="{{t 'search_verb'}}" maxlength="100" autocomplete="off">
</form>
<div class="search-box">
<input class="search-box-input" type="text" name="q" placeholder="{{t 'search_verb'}}" maxlength="100" autocomplete="off">
<svg class="search-box-magnifier" width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<g transform="matrix(0.0288462,0,0,0.0288462,2,1.07692)">
<path d="M288,208C288,177.167 277.042,150.792 255.125,128.875C233.208,106.958 206.833,96 176,96C145.167,96 118.792,106.958 96.875,128.875C74.958,150.792 64,177.167 64,208C64,238.833 74.958,265.208 96.875,287.125C118.792,309.042 145.167,320 176,320C206.833,320 233.208,309.042 255.125,287.125C277.042,265.208 288,238.833 288,208ZM416,416C416,424.667 412.833,432.167 406.5,438.5C400.167,444.833 392.667,448 384,448C375,448 367.5,444.833 361.5,438.5L275.75,353C245.917,373.667 212.667,384 176,384C152.167,384 129.375,379.375 107.625,370.125C85.875,360.875 67.125,348.375 51.375,332.625C35.625,316.875 23.125,298.125 13.875,276.375C4.625,254.625 0,231.833 0,208C0,184.167 4.625,161.375 13.875,139.625C23.125,117.875 35.625,99.125 51.375,83.375C67.125,67.625 85.875,55.125 107.625,45.875C129.375,36.625 152.167,32 176,32C199.833,32 222.625,36.625 244.375,45.875C266.125,55.125 284.875,67.625 300.625,83.375C316.375,99.125 328.875,117.875 338.125,139.625C347.375,161.375 352,184.167 352,208C352,244.667 341.667,277.917 321,307.75L406.75,393.5C412.917,399.667 416,407.167 416,416Z" style="fill:currentColor;fill-rule:nonzero;"/>
</g>
</svg>
<button class="js-reset hidden button-tiny search-box-clear button-icon" style="color: rgb(153, 153, 153);" type="reset">
<svg width="12" height="12" viewBox="0 0 16 16" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<path d="M14 4.242L11.758 2l-3.76 3.76L4.242 2 2 4.242l3.756 3.756L2 11.758 4.242 14l3.756-3.76 3.76 3.76L14 11.758l-3.76-3.76L14 4.242z" style="fill: currentcolor;"/>
</svg>
</button>
</div>
</div>
</div>
<div class="select-list-list-container">

+ 10
- 14
server/sonar-web/src/main/js/components/common/MultiSelect.js Bestand weergeven

@@ -21,6 +21,7 @@
import React from 'react';
import { difference } from 'lodash';
import MultiSelectOption from './MultiSelectOption';
import SearchBox from '../controls/SearchBox';
import { translate } from '../../helpers/l10n';

/*::
@@ -31,7 +32,8 @@ type Props = {
onSearch: string => void,
onSelect: string => void,
onUnselect: string => void,
validateSearchInput: string => string
validateSearchInput: string => string,
placeholder: string
};
*/

@@ -104,8 +106,8 @@ export default class MultiSelect extends React.PureComponent {
}
};

handleSearchChange = ({ target } /*: { target: HTMLInputElement } */) => {
this.onSearchQuery(this.props.validateSearchInput(target.value));
handleSearchChange = (value /*: string */) => {
this.onSearchQuery(this.props.validateSearchInput(value));
};

handleElementHover = (element /*: string */) => {
@@ -232,18 +234,12 @@ export default class MultiSelect extends React.PureComponent {

return (
<div className="multi-select" ref={div => (this.container = div)}>
<div className="search-box menu-search">
<button className="search-box-submit button-clean">
<i className="icon-search-new" />
</button>
<input
type="search"
value={query}
className="search-box-input"
placeholder={translate('search_verb')}
<div className="menu-search">
<SearchBox
autoFocus={true}
onChange={this.handleSearchChange}
autoComplete="off"
ref={input => (this.searchInput = input)}
placeholder={this.props.placeholder}
value={query}
/>
</div>
<ul className="menu">

+ 2
- 1
server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js Bestand weergeven

@@ -26,7 +26,8 @@ const props = {
elements: [],
onSearch: () => {},
onSelect: () => {},
onUnselect: () => {}
onUnselect: () => {},
placeholder: ''
};

const elements = ['foo', 'bar', 'baz'];

+ 16
- 52
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap Bestand weergeven

@@ -5,21 +5,12 @@ exports[`should render multiselect with selected elements 1`] = `
className="multi-select"
>
<div
className="search-box menu-search"
className="menu-search"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search-new"
/>
</button>
<input
autoComplete="off"
className="search-box-input"
<SearchBox
autoFocus={true}
onChange={[Function]}
placeholder="search_verb"
type="search"
placeholder=""
value=""
/>
</div>
@@ -44,21 +35,12 @@ exports[`should render multiselect with selected elements 2`] = `
className="multi-select"
>
<div
className="search-box menu-search"
className="menu-search"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search-new"
/>
</button>
<input
autoComplete="off"
className="search-box-input"
<SearchBox
autoFocus={true}
onChange={[Function]}
placeholder="search_verb"
type="search"
placeholder=""
value=""
/>
</div>
@@ -101,21 +83,12 @@ exports[`should render multiselect with selected elements 3`] = `
className="multi-select"
>
<div
className="search-box menu-search"
className="menu-search"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search-new"
/>
</button>
<input
autoComplete="off"
className="search-box-input"
<SearchBox
autoFocus={true}
onChange={[Function]}
placeholder="search_verb"
type="search"
placeholder=""
value=""
/>
</div>
@@ -158,21 +131,12 @@ exports[`should render multiselect with selected elements 4`] = `
className="multi-select"
>
<div
className="search-box menu-search"
className="menu-search"
>
<button
className="search-box-submit button-clean"
>
<i
className="icon-search-new"
/>
</button>
<input
autoComplete="off"
className="search-box-input"
<SearchBox
autoFocus={true}
onChange={[Function]}
placeholder="search_verb"
type="search"
placeholder=""
value="test"
/>
</div>

server/sonar-web/src/main/js/app/styles/components/search.css → server/sonar-web/src/main/js/components/controls/SearchBox.css Bestand weergeven

@@ -19,39 +19,76 @@
*/
.search-box {
position: relative;
display: inline-block;
vertical-align: middle;
font-size: 0;
white-space: nowrap;
}

.search-box,
.search-box-input {
vertical-align: middle;
width: 250px;
border: none !important;
font-size: var(--baseFontSize);
width: 100%;
max-width: 300px;
}

.search-box-input ~ .note {
opacity: 0;
transition: opacity 0.3s ease;
.search-box-input {
/* for magnifier icon */
padding-left: var(--controlHeight) !important;
/* for clear button */
padding-right: var(--controlHeight) !important;
font-size: var(--baseFontSize);
}

.search-box-input.touched ~ .note {
.search-box-input::placeholder {
color: var(--secondFontColor);
opacity: 1;
}

.search-box-submit {
display: inline-block;
vertical-align: middle;
.search-box-input::-webkit-search-decoration,
.search-box-input::-webkit-search-cancel-button,
.search-box-input::-webkit-search-results-button,
.search-box-input::-webkit-search-results-decoration {
-webkit-appearance: none;
display: none;
}

.search-box-submit .icon-search:before {
color: var(--secondFontColor);
font-size: var(--mediumFontSize);
.search-box-input::-ms-clear,
.search-box-input::-ms-reveal {
display: none;
width: 0;
height: 0;
}

.search-box-submit .icon-search-new {
position: relative;
.search-box-note {
position: absolute;
top: 1px;
left: 40px;
right: var(--controlHeight);
line-height: calc(var(--controlHeight));
color: var(--secondFontColor);
font-size: var(--smallFontSize);
text-align: right;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}

.search-box-input:focus ~ .search-box-magnifier {
color: var(--blue);
}

.search-box-magnifier {
position: absolute;
top: 4px;
left: 4px;
color: var(--gray60);
transition: color 0.3s ease;
}

.search-box-clear {
position: absolute;
top: 4px;
right: 4px;
}

.search-box-input-note {

+ 167
- 0
server/sonar-web/src/main/js/components/controls/SearchBox.tsx Bestand weergeven

@@ -0,0 +1,167 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact 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 { debounce, Cancelable } from 'lodash';
import SearchIcon from '../icons-components/SearchIcon';
import ClearIcon from '../icons-components/ClearIcon';
import { ButtonIcon } from '../ui/buttons';
import * as theme from '../../app/theme';
import { translateWithParameters } from '../../helpers/l10n';
import './SearchBox.css';

interface Props {
autoFocus?: boolean;
innerRef?: (node: HTMLInputElement | null) => void;
minLength?: number;
onChange: (value: string) => void;
onClick?: React.MouseEventHandler<HTMLInputElement>;
onFocus?: React.FocusEventHandler<HTMLInputElement>;
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
placeholder: string;
value?: string;
}

interface State {
value: string;
}

export default class SearchBox extends React.PureComponent<Props, State> {
debouncedOnChange: ((query: string) => void) & Cancelable;
input: HTMLInputElement | null;

constructor(props: Props) {
super(props);
this.state = { value: props.value || '' };
this.debouncedOnChange = debounce(this.props.onChange, 250);
}

componentWillReceiveProps(nextProps: Props) {
if (
// input is controlled
nextProps.value !== undefined &&
// parent is aware of last change
// can happen when previous value was less than min length
this.state.value === this.props.value &&
nextProps.value !== this.state.value
) {
this.setState({ value: nextProps.value });
}
}

changeValue = (value: string, debounced = true) => {
const { minLength } = this.props;
if (value.length === 0) {
// immediately notify when value is empty
this.props.onChange('');
// and cancel scheduled callback
this.debouncedOnChange.cancel();
} else if (!minLength || minLength <= value.length) {
if (debounced) {
this.debouncedOnChange(value);
} else {
this.props.onChange(value);
}
}
};

handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const { value } = event.currentTarget;
this.setState({ value });
this.changeValue(value);
};

handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.keyCode === 27) {
// escape
event.preventDefault();
this.handleResetClick();
}
if (this.props.onKeyDown) {
this.props.onKeyDown(event);
}
};

handleResetClick = () => {
this.changeValue('', false);
if (this.props.value === undefined) {
this.setState({ value: '' });
}
if (this.input) {
this.input.focus();
}
};

ref = (node: HTMLInputElement | null) => {
this.input = node;
if (this.props.innerRef) {
this.props.innerRef(node);
}
};

render() {
const { minLength } = this.props;
const { value } = this.state;

const inputClassName = classNames('search-box-input', {
touched: value.length > 0 && (!minLength || minLength > value.length)
});

const tooShort = minLength !== undefined && value.length > 0 && value.length < minLength;

return (
<div className="search-box">
<input
autoComplete="off"
autoFocus={this.props.autoFocus}
className={inputClassName}
maxLength={100}
onChange={this.handleInputChange}
onClick={this.props.onClick}
onFocus={this.props.onFocus}
onKeyDown={this.handleInputKeyDown}
placeholder={this.props.placeholder}
ref={this.ref}
type="search"
value={value}
/>

<SearchIcon className="search-box-magnifier" />

{value && (
<ButtonIcon
className="button-tiny search-box-clear"
color={theme.gray60}
onClick={this.handleResetClick}>
<ClearIcon size={12} />
</ButtonIcon>
)}

{tooShort && (
<span
className="search-box-note"
title={translateWithParameters('select2.tooShort', minLength!)}>
{translateWithParameters('select2.tooShort', minLength!)}
</span>
)}
</div>
);
}
}

+ 84
- 0
server/sonar-web/src/main/js/components/controls/__tests__/SearchBox-test.tsx Bestand weergeven

@@ -0,0 +1,84 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact 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, mount } from 'enzyme';
import SearchBox from '../SearchBox';
import { click, change } from '../../../helpers/testUtils';

jest.mock('lodash', () => {
const lodash = require.requireActual('lodash');
const debounce = (fn: Function) => {
const debounced: any = (...args: any[]) => fn(...args);
debounced.cancel = jest.fn();
return debounced;
};
return Object.assign({}, lodash, { debounce });
});

it('renders', () => {
const wrapper = shallow(
<SearchBox minLength={2} onChange={jest.fn()} placeholder="placeholder" value="foo" />
);
expect(wrapper).toMatchSnapshot();
});

it('warns when input is too short', () => {
const wrapper = shallow(
<SearchBox minLength={2} onChange={jest.fn()} placeholder="placeholder" value="f" />
);
expect(wrapper.find('.search-box-note').exists()).toBeTruthy();
});

it('shows clear button only when there is a value', () => {
const wrapper = shallow(<SearchBox onChange={jest.fn()} placeholder="placeholder" value="f" />);
expect(wrapper.find('.search-box-clear').exists()).toBeTruthy();
wrapper.setProps({ value: '' });
expect(wrapper.find('.search-box-clear').exists()).toBeFalsy();
});

it('attaches ref', () => {
const ref = jest.fn();
mount(<SearchBox innerRef={ref} onChange={jest.fn()} placeholder="placeholder" value="f" />);
expect(ref).toBeCalled();
expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLInputElement);
});

it('resets', () => {
const onChange = jest.fn();
const wrapper = shallow(<SearchBox onChange={onChange} placeholder="placeholder" value="f" />);
click(wrapper.find('.search-box-clear'));
expect(onChange).toBeCalledWith('');
});

it('changes', () => {
const onChange = jest.fn();
const wrapper = shallow(<SearchBox onChange={onChange} placeholder="placeholder" value="f" />);
change(wrapper.find('.search-box-input'), 'foo');
expect(onChange).toBeCalledWith('foo');
});

it('does not change when value is too short', () => {
const onChange = jest.fn();
const wrapper = shallow(
<SearchBox minLength={3} onChange={onChange} placeholder="placeholder" value="" />
);
change(wrapper.find('.search-box-input'), 'fo');
expect(onChange).not.toBeCalled();
});

+ 30
- 0
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap Bestand weergeven

@@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders 1`] = `
<div
className="search-box"
>
<input
autoComplete="off"
className="search-box-input"
maxLength={100}
onChange={[Function]}
onKeyDown={[Function]}
placeholder="placeholder"
type="search"
value="foo"
/>
<SearchIcon
className="search-box-magnifier"
/>
<ButtonIcon
className="button-tiny search-box-clear"
color="#999"
onClick={[Function]}
>
<ClearIcon
size={12}
/>
</ButtonIcon>
</div>
`;

+ 39
- 0
server/sonar-web/src/main/js/components/icons-components/SearchIcon.tsx Bestand weergeven

@@ -0,0 +1,39 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:contact 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 { IconProps } from './types';

export default function SearchIcon({ className, fill = 'currentColor', size = 16 }: IconProps) {
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 16 16"
version="1.1"
xmlnsXlink="http://www.w3.org/1999/xlink"
xmlSpace="preserve">
<path
style={{ fill }}
d="M10.308 7.077c0-.89-.316-1.65-.949-2.283a3.111 3.111 0 0 0-2.282-.948c-.89 0-1.65.316-2.283.948a3.111 3.111 0 0 0-.948 2.283c0 .89.316 1.65.948 2.282a3.111 3.111 0 0 0 2.283.949c.89 0 1.65-.316 2.282-.949a3.111 3.111 0 0 0 .949-2.282zm3.692 6c0 .25-.091.466-.274.649a.887.887 0 0 1-.65.274.857.857 0 0 1-.648-.274L9.954 11.26c-.86.596-1.82.894-2.877.894a4.989 4.989 0 0 1-1.972-.4 5.076 5.076 0 0 1-1.623-1.082A5.076 5.076 0 0 1 2.4 9.049 4.989 4.989 0 0 1 2 7.077c0-.688.133-1.345.4-1.972a5.076 5.076 0 0 1 1.082-1.623A5.076 5.076 0 0 1 5.105 2.4 4.989 4.989 0 0 1 7.077 2c.687 0 1.345.133 1.972.4a5.076 5.076 0 0 1 1.623 1.082c.454.454.815.995 1.082 1.623.266.627.4 1.284.4 1.972a4.938 4.938 0 0 1-.894 2.877l2.473 2.474a.883.883 0 0 1 .267.649z"
/>
</svg>
);
}

+ 10
- 17
server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js Bestand weergeven

@@ -19,11 +19,12 @@
*/
// @flow
import React from 'react';
import { debounce, map } from 'lodash';
import { map } from 'lodash';
import Avatar from '../../../components/ui/Avatar';
import BubblePopup from '../../../components/common/BubblePopup';
import SelectList from '../../../components/common/SelectList';
import SelectListItem from '../../../components/common/SelectListItem';
import SearchBox from '../../../components/controls/SearchBox';
import getCurrentUserFromStore from '../../../app/utils/getCurrentUserFromStore';
import { areThereCustomOrganizations } from '../../../store/organizations/utils';
import { searchMembers } from '../../../api/organizations';
@@ -68,8 +69,6 @@ export default class SetAssigneePopup extends React.PureComponent {
constructor(props /*: Props */) {
super(props);
this.organizationEnabled = areThereCustomOrganizations();
this.searchUsers = debounce(this.searchUsers, 250);
this.searchMembers = debounce(this.searchMembers, 250);
this.defaultUsersArray = [{ login: '', name: translate('unassigned') }];

const currentUser = getCurrentUserFromStore();
@@ -103,9 +102,8 @@ export default class SetAssigneePopup extends React.PureComponent {
});
};

handleSearchChange = (evt /*: SyntheticInputEvent */) => {
const query = evt.target.value;
if (query.length < 2) {
handleSearchChange = (query /*: string */) => {
if (query.length === 0) {
this.setState({
query,
users: this.defaultUsersArray,
@@ -127,18 +125,13 @@ export default class SetAssigneePopup extends React.PureComponent {
position={this.props.popupPosition}
customClass="bubble-popup-menu bubble-popup-bottom">
<div className="multi-select">
<div className="search-box menu-search">
<button className="search-box-submit button-clean">
<i className="icon-search-new" />
</button>
<input
type="search"
value={this.state.query}
className="search-box-input"
placeholder={translate('search_verb')}
onChange={this.handleSearchChange}
autoComplete="off"
<div className="menu-search">
<SearchBox
autoFocus={true}
minLength={2}
onChange={this.handleSearchChange}
placeholder={translate('search.search_for_users')}
value={this.state.query}
/>
</div>
<SelectList

+ 4
- 9
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js Bestand weergeven

@@ -19,7 +19,7 @@
*/
//@flow
import React from 'react';
import { debounce, without } from 'lodash';
import { without } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { searchIssueTags } from '../../../api/issues';

@@ -44,13 +44,7 @@ const LIST_SIZE = 10;
export default class SetIssueTagsPopup extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
/*:: state: State; */

constructor(props /*: Props */) {
super(props);
this.state = { searchResult: [] };
this.onSearch = debounce(this.onSearch, 250);
}
state /*: State */ = { searchResult: [] };

componentDidMount() {
this.mounted = true;
@@ -63,7 +57,7 @@ export default class SetIssueTagsPopup extends React.PureComponent {

onSearch = (query /*: string */) => {
searchIssueTags({
q: query || '',
q: query,
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
organization: this.props.organization
}).then((tags /*: Array<string> */) => {
@@ -83,6 +77,7 @@ export default class SetIssueTagsPopup extends React.PureComponent {

render() {
return (
// $FlowFixMe `this.props.popupPosition` is passed from `BabelPopupHelper`
<TagsSelector
position={this.props.popupPosition}
tags={this.state.searchResult}

+ 22
- 26
server/sonar-web/src/main/js/components/tags/TagsSelector.js Bestand weergeven

@@ -21,6 +21,7 @@
import React from 'react';
import BubblePopup from '../common/BubblePopup';
import MultiSelect from '../common/MultiSelect';
import { translate } from '../../helpers/l10n';
import './TagsList.css';

/*::
@@ -35,31 +36,26 @@ type Props = {
};
*/

export default class TagsSelector extends React.PureComponent {
/*:: validateTag: string => string; */

/*:: props: Props; */

validateTag(value /*: string */) {
// Allow only a-z, 0-9, '+', '-', '#', '.'
return value.toLowerCase().replace(/[^a-z0-9\+\-#.]/gi, '');
}
export default function TagsSelector(props /*: Props */) {
return (
<BubblePopup
position={props.position}
customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300">
<MultiSelect
elements={props.tags}
selectedElements={props.selectedTags}
listSize={props.listSize}
onSearch={props.onSearch}
onSelect={props.onSelect}
onUnselect={props.onUnselect}
validateSearchInput={validateTag}
placeholder={translate('search.search_for_tags')}
/>
</BubblePopup>
);
}

render() {
return (
<BubblePopup
position={this.props.position}
customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300">
<MultiSelect
elements={this.props.tags}
selectedElements={this.props.selectedTags}
listSize={this.props.listSize}
onSearch={this.props.onSearch}
onSelect={this.props.onSelect}
onUnselect={this.props.onUnselect}
validateSearchInput={this.validateTag}
/>
</BubblePopup>
);
}
export function validateTag(value /*: string */) {
// Allow only a-z, 0-9, '+', '-', '#', '.'
return value.toLowerCase().replace(/[^a-z0-9\+\-#.]/gi, '');
}

+ 6
- 7
server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js Bestand weergeven

@@ -19,7 +19,7 @@
*/
import { shallow } from 'enzyme';
import React from 'react';
import TagsSelector from '../TagsSelector';
import TagsSelector, { validateTag } from '../TagsSelector';

const props = {
position: { left: 0, top: 0 },
@@ -41,10 +41,9 @@ it('should render without tags at all', () => {

it('should validate tags correctly', () => {
const validChars = 'abcdefghijklmnopqrstuvwxyz0123456789+-#.';
const tagsSelector = shallow(<TagsSelector {...props} />).instance();
expect(tagsSelector.validateTag('test')).toBe('test');
expect(tagsSelector.validateTag(validChars)).toBe(validChars);
expect(tagsSelector.validateTag(validChars.toUpperCase())).toBe(validChars);
expect(tagsSelector.validateTag('T E$ST')).toBe('test');
expect(tagsSelector.validateTag('T E$st!^àéèing1')).toBe('testing1');
expect(validateTag('test')).toBe('test');
expect(validateTag(validChars)).toBe(validChars);
expect(validateTag(validChars.toUpperCase())).toBe(validChars);
expect(validateTag('T E$ST')).toBe('test');
expect(validateTag('T E$st!^àéèing1')).toBe('testing1');
});

+ 2
- 0
server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap Bestand weergeven

@@ -22,6 +22,7 @@ exports[`should render with selected tags 1`] = `
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
placeholder="search.search_for_tags"
selectedElements={
Array [
"bar",
@@ -48,6 +49,7 @@ exports[`should render without tags at all 1`] = `
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
placeholder="search.search_for_tags"
selectedElements={Array []}
validateSearchInput={[Function]}
/>

+ 1
- 1
server/sonar-web/src/main/js/components/ui/buttons.tsx Bestand weergeven

@@ -43,7 +43,7 @@ export class ButtonIcon extends React.PureComponent<ButtonIconProps> {
};

render() {
const { children, className, color = theme.darkBlue, ...props } = this.props;
const { children, className, color = theme.darkBlue, onClick, ...props } = this.props;
return (
<button
className={classNames(className, 'button-icon')}

+ 9
- 2
server/sonar-web/src/main/js/helpers/testUtils.ts Bestand weergeven

@@ -56,11 +56,18 @@ export function keydown(keyCode: number): void {
}

export function elementKeydown(element: ShallowWrapper, keyCode: number): void {
element.simulate('keydown', {
const event = {
currentTarget: { element },
keyCode,
preventDefault() {}
});
};

if (typeof element.type() === 'string') {
// `type()` is string for native dom elements
element.simulate('keydown', event);
} else {
element.prop<Function>('onKeyDown')(event);
}
}

export function doAsync(fn?: Function): Promise<void> {

+ 15
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Bestand weergeven

@@ -834,9 +834,18 @@ property.sonar.branch.longLivedBranches.regex.description=Regular expression use
# SEARCH ENGINE FOR RESOURCES
#
#------------------------------------------------------------------------------
search.shortcut_hint=Hint: Press {0} from anywhere to open this search bar.
search.shortcut_hint=Hint: Press {shortcut} from anywhere to open this search bar.
search.show_more.hint=Press {0} to display
search.placeholder=Search for projects, sub-projects and files...
search.search_for_projects=Search for projects...
search.search_for_users=Search for users...
search.search_for_users_or_groups=Search for users or groups...
search.search_by_login_or_name=Search by login or name...
search.search_by_name=Search by name...
search.search_by_name_or_key=Search by name or key...
search.search_for_tags=Search for tags...
search.search_for_rules=Search for rules...
search.search_for_languages=Search for languages...


#------------------------------------------------------------------------------
@@ -2125,6 +2134,7 @@ marketplace.enter_license_for_x=Enter your license key for {0}
marketplace.wrong_license_type_x=Your license is not compatible with the selected edition. Please provide a valid license for {0}.
marketplace.i_need_a_license=I need a license key
marketplace.download_package=Download package
marketplace.search=Search by features or categories...


#------------------------------------------------------------------------------
@@ -2310,6 +2320,7 @@ api_documentation.deprecated_since_x=deprecated since {0}
api_documentation.parameters=Parameters
api_documentation.response_example=Response Example
api_documentation.changelog=Changelog
api_documentation.search=Search by name...


#------------------------------------------------------------------------------
@@ -2318,6 +2329,8 @@ api_documentation.changelog=Changelog
#
#------------------------------------------------------------------------------
code.open_component_page=Open Component's Page
code.search_placeholder=Search for files and sub-projects...
code.search_placeholder.portfolio=Search for projects and sub-portfolios...


#------------------------------------------------------------------------------
@@ -2567,6 +2580,7 @@ branches.set_leak_period=Set Leak Period
branches.last_analysis_date=Last Analysis Date
branches.no_support.header=Get the most out of SonarQube with branches analysis
branches.no_support.header.text=Analyze each branch of your project separately with the Developer Edition.
branches.search_for_branches=Search for branches...


#------------------------------------------------------------------------------

+ 14
- 2
tests/src/test/java/org/sonarqube/pageobjects/Navigation.java Bestand weergeven

@@ -31,12 +31,12 @@ import javax.annotation.Nullable;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.html5.WebStorage;
import org.sonarqube.pageobjects.measures.MeasuresPage;
import org.sonarqube.tests.Tester;
import org.sonarqube.pageobjects.issues.IssuesPage;
import org.sonarqube.pageobjects.measures.MeasuresPage;
import org.sonarqube.pageobjects.organization.MembersPage;
import org.sonarqube.pageobjects.projects.ProjectsPage;
import org.sonarqube.pageobjects.settings.SettingsPage;
import org.sonarqube.tests.Tester;

import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
@@ -142,6 +142,18 @@ public class Navigation {
return open(url, MeasuresPage.class);
}

public ProjectCodePage openCode(String projectKey) {
// TODO encode projectKey
String url = "/code?id=" + projectKey;
return open(url, ProjectCodePage.class);
}

public ProjectCodePage openCode(String projectKey, String selected) {
// TODO encode projectKey and selected
String url = "/code?id=" + projectKey + "&selected=" + selected;
return open(url, ProjectCodePage.class);
}

public MembersPage openOrganizationMembers(String orgKey) {
String url = "/organizations/" + orgKey + "/members";
return open(url, MembersPage.class);

+ 61
- 0
tests/src/test/java/org/sonarqube/pageobjects/ProjectCodePage.java Bestand weergeven

@@ -0,0 +1,61 @@
/*
* SonarQube
* Copyright (C) 2009-2017 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.
*/
package org.sonarqube.pageobjects;

import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.$$;

public class ProjectCodePage {

public ProjectCodePage() {}

public ProjectCodePage openFirstComponent() {
$$(".code-name-cell a").first().click();
return this;
}

public ProjectCodePage search(String query) {
$(".code-search .search-box-input").val(query);
return this;
}

public ProjectCodePage shouldHaveComponent(String name) {
$(".code-components").shouldHave(text(name));
return this;
}

public ProjectCodePage shouldHaveCode(String code) {
$(".code-components .source-viewer").shouldHave(text(code));
return this;
}

public ProjectCodePage shouldHaveBreadcrumbs(String... breadcrumbs) {
for (String breadcrumb : breadcrumbs) {
$(".code-breadcrumbs").shouldHave(text(breadcrumb));
}
return this;
}

public ProjectCodePage shouldSearchResult(String name) {
$(".code-search-with-results").shouldHave(text(name));
return this;
}
}

+ 1
- 1
tests/src/test/java/org/sonarqube/pageobjects/organization/MembersPage.java Bestand weergeven

@@ -49,7 +49,7 @@ public class MembersPage {
}

public MembersPage searchForMember(String query) {
$("input.search-box-input").shouldBe(visible).val("").sendKeys(query);
$(".page .search-box-input").shouldBe(visible).val("").sendKeys(query);
return this;
}


+ 9
- 12
tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipUiTest.java Bestand weergeven

@@ -21,19 +21,16 @@
package org.sonarqube.tests.organization;

import com.sonar.orchestrator.Orchestrator;
import org.sonarqube.tests.Category6Suite;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.sonarqube.tests.OrganizationTester;
import org.sonarqube.pageobjects.organization.MembersPage;
import org.sonarqube.tests.Category6Suite;
import org.sonarqube.tests.Tester;
import org.sonarqube.ws.Organizations.Organization;
import org.sonarqube.ws.WsUsers.CreateWsResponse.User;
import org.sonarqube.pageobjects.organization.MembersPage;

import static util.ItUtils.setServerProperty;

public class OrganizationMembershipUiTest {

@@ -47,14 +44,14 @@ public class OrganizationMembershipUiTest {

@Before
public void setUp() {
setServerProperty(orchestrator, "sonar.organizations.anyoneCanCreate", "true");
tester.settings().setGlobalSetting("sonar.organizations.anyoneCanCreate", "true");
root = tester.users().generate();
tester.wsClient().roots().setRoot(root.getLogin());
}

@After
public void tearDown() {
setServerProperty(orchestrator, "sonar.organizations.anyoneCanCreate", null);
tester.settings().resetSettings("sonar.organizations.anyoneCanCreate");
}

@Test
@@ -64,7 +61,7 @@ public class OrganizationMembershipUiTest {
addMember(organization, member1);
User member2 = tester.users().generate(p -> p.setName("bar"));
addMember(organization, member2);
User nonMember = tester.users().generate();
tester.users().generate();

MembersPage page = tester.openBrowser().openOrganizationMembers(organization.getKey());
page
@@ -86,7 +83,7 @@ public class OrganizationMembershipUiTest {
User member2 = tester.users().generate(p -> p.setName("sameprefixuser1"));
addMember(organization, member2);
// Created to verify that only the user part of the org is returned
User userWithSameNamePrefix = tester.users().generate(p -> p.setName(member2.getName() + "sameprefixuser2"));
tester.users().generate(p -> p.setName(member2.getName() + "sameprefixuser2"));

MembersPage page = tester.openBrowser().openOrganizationMembers(organization.getKey());
page
@@ -103,7 +100,7 @@ public class OrganizationMembershipUiTest {
public void admin_can_add_members() {
Organization organization = tester.organizations().generate();
User user1 = tester.users().generate(u -> u.setLogin("foo"));
User user2 = tester.users().generate();
tester.users().generate();

MembersPage page = tester.openBrowser()
.logIn().submitCredentials(root.getLogin())
@@ -173,7 +170,7 @@ public class OrganizationMembershipUiTest {
.shouldHaveGroups(2);
}

private OrganizationTester addMember(Organization organization, User member1) {
return tester.organizations().addMember(organization, member1);
private void addMember(Organization organization, User member1) {
tester.organizations().addMember(organization, member1);
}
}

+ 26
- 26
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeletionPageTest.java Bestand weergeven

@@ -20,37 +20,34 @@
package org.sonarqube.tests.projectAdministration;

import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import org.sonarqube.tests.Category1Suite;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import util.user.UserRule;
import org.sonarqube.tests.Category1Suite;
import org.sonarqube.tests.Tester;
import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
import org.sonarqube.ws.client.component.SearchProjectsRequest;

import static util.ItUtils.projectDir;
import static util.selenium.Selenese.runSelenese;
import static com.codeborne.selenide.Condition.text;
import static com.codeborne.selenide.Condition.visible;
import static com.codeborne.selenide.Selenide.$;
import static org.assertj.core.api.Assertions.assertThat;

public class ProjectBulkDeletionPageTest {

private static final String ADMIN_USER_LOGIN = "admin-user";
private String adminUser;

@ClassRule
public static Orchestrator orchestrator = Category1Suite.ORCHESTRATOR;

@Rule
public UserRule userRule = UserRule.from(orchestrator);
public Tester tester = new Tester(orchestrator);

@Before
public void deleteData() {
orchestrator.resetData();
userRule.createAdminUser(ADMIN_USER_LOGIN, ADMIN_USER_LOGIN);
}

@After
public void deleteAdminUser() {
userRule.resetUsers();
adminUser = tester.users().generateAdministrator().getLogin();
}

/**
@@ -58,19 +55,22 @@ public class ProjectBulkDeletionPageTest {
*/
@Test
public void test_bulk_deletion_on_selected_projects() throws Exception {
// we must have several projects to test the bulk deletion
executeBuild("cameleon-1", "Sample-Project");
executeBuild("cameleon-2", "Foo-Application");
executeBuild("cameleon-3", "Bar-Sonar-Plugin");
Project project1 = tester.projects().generate(null, t -> t.setName("Foo"));
Project project2 = tester.projects().generate(null, t -> t.setName("Bar"));
Project project3 = tester.projects().generate(null, t -> t.setName("FooQux"));

runSelenese(orchestrator, "/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html");
}
tester.openBrowser().logIn().submitCredentials(adminUser).open("/admin/projects_management");
$("#projects-management-page").shouldHave(text(project1.getName())).shouldHave(text(project2.getName())).shouldHave(text(project3.getName()));

private void executeBuild(String projectKey, String projectName) {
orchestrator.executeBuild(
SonarScanner.create(projectDir("shared/xoo-sample"))
.setProjectKey(projectKey)
.setProjectName(projectName));
}
$("#projects-management-page .search-box-input").val("foo").pressEnter();
$("#projects-management-page").shouldNotHave(text(project2.getName())).shouldHave(text(project1.getName())).shouldHave(text(project3.getName()));

$("#projects-management-page .js-delete").click();
$(".modal").shouldBe(visible);
$(".modal button").click();
$("#projects-management-page").shouldNotHave(text(project1.getName())).shouldNotHave(text(project3.getName()));

assertThat(tester.wsClient().components().searchProjects(SearchProjectsRequest.builder().build())
.getComponentsCount()).isEqualTo(1);
}
}

+ 46
- 14
tests/src/test/java/org/sonarqube/tests/sourceCode/ProjectCodeTest.java Bestand weergeven

@@ -21,11 +21,12 @@ package org.sonarqube.tests.sourceCode;

import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
import org.sonarqube.tests.Category1Suite;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.sonarqube.tests.Category1Suite;
import org.sonarqube.tests.Tester;
import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;

import static util.ItUtils.projectDir;

@@ -38,27 +39,58 @@ public class ProjectCodeTest {
public Tester tester = new Tester(orchestrator).disableOrganizations();

@Test
public void test_project_code_page() {
executeBuild("shared/xoo-sample", "project-for-code", "Project For Code");
public void browse() {
Project project = tester.projects().generate(null);
executeAnalysis(project);

tester.openBrowser().openCode(project.getKey())
.shouldHaveComponent("src/main/xoo/sample")
.openFirstComponent()
.shouldHaveComponent("Sample.xoo")
.openFirstComponent()
.shouldHaveCode("public class Sample")
.shouldHaveBreadcrumbs(project.getName(), "src/main/xoo/sample", "Sample.xoo");
}

@Test
public void search() {
Project project = tester.projects().generate(null);
executeAnalysis(project);

tester.runHtmlTests(
"/sourceCode/ProjectCodeTest/test_project_code_page.html",
"/sourceCode/ProjectCodeTest/search.html",
"/sourceCode/ProjectCodeTest/permalink.html");
tester.openBrowser().openCode(project.getKey())
.shouldHaveComponent(project.getName())
.search("xoo")
.shouldSearchResult("Sample.xoo");
}

@Test
public void code_page_should_expand_root_dir() {
executeBuild("shared/xoo-sample-with-root-dir", "project-for-code-root-dir", "Project For Code");
public void permalink() {
Project project = tester.projects().generate(null);
executeAnalysis(project);

tester.runHtmlTests("/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html");
tester.openBrowser().openCode(project.getKey(), project.getKey() + "%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo")
.shouldHaveCode("public class Sample")
.shouldHaveBreadcrumbs(project.getName(), "src/main/xoo/sample", "Sample.xoo");
}

private void executeBuild(String projectLocation, String projectKey, String projectName) {
@Test
public void expand_root_dir() {
Project project = tester.projects().generate(null);
executeAnalysis(project, "shared/xoo-sample-with-root-dir");

tester.openBrowser().openCode(project.getKey())
.shouldHaveComponent("Hello.xoo")
.shouldHaveComponent("src/main/xoo/sample");
}

private void executeAnalysis(Project project, String path) {
orchestrator.executeBuild(
SonarScanner.create(projectDir(projectLocation))
.setProjectKey(projectKey)
.setProjectName(projectName));
SonarScanner.create(projectDir(path))
.setProjectKey(project.getKey())
.setProjectName(project.getName()));
}

private void executeAnalysis(Project project) {
executeAnalysis(project, "shared/xoo-sample");
}
}

+ 0
- 74
tests/src/test/resources/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html Bestand weergeven

@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>bulk-delete-filter-projects</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<tbody>
<tr>
<td>open</td>
<td>/sessions/login</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>login</td>
<td>admin-user</td>
</tr>
<tr>
<td>type</td>
<td>password</td>
<td>admin-user</td>
</tr>
<tr>
<td>clickAndWait</td>
<td>commit</td>
<td></td>
</tr>
<tr>
<td>waitForElementPresent</td>
<td>css=.js-user-authenticated</td>
<td></td>
</tr>
<tr>
<td>open</td>
<td>/projects_admin</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*Bar-Sonar-Plugin*Foo-Application*Sample-Project*</td>
</tr>
<tr>
<td>type</td>
<td>css=.search-box-input</td>
<td>s</td>
</tr>
<tr>
<td>click</td>
<td>css=.search-box-submit</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*Bar-Sonar-Plugin*Sample-Project*</td>
</tr>
<tr>
<td>waitForText</td>
<td>content</td>
<td>*cameleon-3*cameleon-1*</td>
</tr>
<tr>
<td>assertTextNotPresent</td>
<td>content</td>
<td>*Foo-Application*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 30
tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html Bestand weergeven

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="selenium.base" href="http://localhost:49506"/>
<title>code_page_should_expand_root_dir</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">code_page_should_expand_root_dir</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/code?id=project-for-code-root-dir</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content</td>
<td>*Hello.xoo*src/main/xoo/sample*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 35
tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html Bestand weergeven

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="selenium.base" href="http://localhost:49506"/>
<title>test_project_code_page</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">test_project_code_page</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/code?id=project-for-code&amp;selected=project-for-code%3Asrc%2Fmain%2Fxoo%2Fsample%2FSample.xoo</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content</td>
<td>*public class Sample*</td>
</tr>
<tr>
<td>waitForText</td>
<td>css=.code-breadcrumbs</td>
<td>*Project For Code*src/main/xoo/sample*Sample.xoo*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 60
tests/src/test/resources/sourceCode/ProjectCodeTest/search.html Bestand weergeven

@@ -1,60 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="selenium.base" href="http://localhost:49506"/>
<title>test_project_code_page</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">test_project_code_page</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/code?id=project-for-code</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content</td>
<td>*Project For Code*13*0*0*0.0%*</td>
</tr>
<tr>
<td>type</td>
<td>css=.search-box-input</td>
<td>xoo</td>
</tr>
<tr>
<td>click</td>
<td>css=.search-box-submit</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content</td>
<td>*Sample.xoo*</td>
</tr>
<tr>
<td>click</td>
<td>css=.code-name-cell a</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content</td>
<td>*public class Sample*</td>
</tr>
<tr>
<td>waitForText</td>
<td>css=.code-breadcrumbs</td>
<td>*Project For Code*src/main/xoo/sample*Sample.xoo*</td>
</tr>
</tbody>
</table>
</body>
</html>

+ 0
- 55
tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html Bestand weergeven

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="selenium.base" href="http://localhost:49506"/>
<title>test_project_code_page</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr>
<td rowspan="1" colspan="3">test_project_code_page</td>
</tr>
</thead>
<tbody>
<tr>
<td>open</td>
<td>/code?id=project-for-code</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content</td>
<td>*Project For Code*13*0*0*0.0%*</td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content</td>
<td>*src/main/xoo/sample*</td>
</tr>
<tr>
<td>click</td>
<td>css=.code-name-cell a</td>
<td></td>
</tr>
<tr>
<td>waitForText</td>
<td>css=#content</td>
<td>*Sample.xoo*</td>
</tr>
<tr>
<td>click</td>
<td>css=.code-breadcrumbs a</td>
<td></td>
</tr>
<tr>
<td>waitForNotText</td>
<td>css=#content</td>
<td>*Sample.xoo*</td>
</tr>
</tbody>
</table>
</body>
</html>

Laden…
Annuleren
Opslaan