]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9225 Make all search bars consistent
authorStas Vilchik <stas.vilchik@sonarsource.com>
Thu, 2 Nov 2017 16:50:18 +0000 (17:50 +0100)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Fri, 3 Nov 2017 13:28:18 +0000 (14:28 +0100)
81 files changed:
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/search/Search.css
server/sonar-web/src/main/js/app/components/search/Search.js
server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.js
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/Search-test.js.snap
server/sonar-web/src/main/js/app/styles/components/menu.css
server/sonar-web/src/main/js/app/styles/components/search-navigator.css
server/sonar-web/src/main/js/app/styles/components/search.css [deleted file]
server/sonar-web/src/main/js/app/styles/init/icons.css
server/sonar-web/src/main/js/app/styles/sonar.css
server/sonar-web/src/main/js/apps/background-tasks/__tests__/background-tasks-test.js
server/sonar-web/src/main/js/apps/background-tasks/background-tasks.css
server/sonar-web/src/main/js/apps/background-tasks/components/Search.js
server/sonar-web/src/main/js/apps/code/code.css
server/sonar-web/src/main/js/apps/code/components/Search.tsx
server/sonar-web/src/main/js/apps/coding-rules/facets/query-facet.js
server/sonar-web/src/main/js/apps/coding-rules/templates/facets/coding-rules-query-facet.hbs
server/sonar-web/src/main/js/apps/groups/components/GroupsAppContainer.js
server/sonar-web/src/main/js/apps/groups/search-view.js
server/sonar-web/src/main/js/apps/groups/templates/groups-search.hbs
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacetFooter.js
server/sonar-web/src/main/js/apps/marketplace/Search.tsx
server/sonar-web/src/main/js/apps/organizations/components/MembersListHeader.js
server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/MembersListHeader-test.js.snap
server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js
server/sonar-web/src/main/js/apps/permission-templates/components/Template.js
server/sonar-web/src/main/js/apps/permissions/global/store/actions.js
server/sonar-web/src/main/js/apps/permissions/project/components/App.js
server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js
server/sonar-web/src/main/js/apps/projects/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/SearchFilterContainer.tsx
server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilter-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilterContainer-test.tsx
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilterContainer-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/styles.css
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap
server/sonar-web/src/main/js/apps/users/components/UsersAppContainer.js
server/sonar-web/src/main/js/apps/users/components/UsersSearch.js [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js [deleted file]
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/users/search-view.js
server/sonar-web/src/main/js/apps/users/templates/users-search.hbs
server/sonar-web/src/main/js/apps/web-api/components/Search.tsx
server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Search-test.tsx.snap
server/sonar-web/src/main/js/apps/web-api/styles/web-api.css
server/sonar-web/src/main/js/components/SelectList/index.js
server/sonar-web/src/main/js/components/SelectList/templates/list.hbs
server/sonar-web/src/main/js/components/common/MultiSelect.js
server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.js
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.js.snap
server/sonar-web/src/main/js/components/controls/SearchBox.css [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/SearchBox.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/SearchBox-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/icons-components/SearchIcon.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
server/sonar-web/src/main/js/components/tags/TagsSelector.js
server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.js
server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.js.snap
server/sonar-web/src/main/js/components/ui/buttons.tsx
server/sonar-web/src/main/js/helpers/testUtils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties
tests/src/test/java/org/sonarqube/pageobjects/Navigation.java
tests/src/test/java/org/sonarqube/pageobjects/ProjectCodePage.java [new file with mode: 0644]
tests/src/test/java/org/sonarqube/pageobjects/organization/MembersPage.java
tests/src/test/java/org/sonarqube/tests/organization/OrganizationMembershipUiTest.java
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectBulkDeletionPageTest.java
tests/src/test/java/org/sonarqube/tests/sourceCode/ProjectCodeTest.java
tests/src/test/resources/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html [deleted file]
tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html [deleted file]
tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html [deleted file]
tests/src/test/resources/sourceCode/ProjectCodeTest/search.html [deleted file]
tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html [deleted file]

index a4d7899387bb4aced0a5d560780804666ae569ec..07f7e0508cdf68ea7f3d20762d715e3eb6330b0d 100644 (file)
@@ -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>
index 6232c936b4dedcf8bc0ef71b0f87a6d6595eae63..ae5b7a80d72aec409cc9409af65864d2961d5c61 100644 (file)
@@ -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');
 });
index 7a01722ba4cc69b3262faca63c398e1f27159846..6f39c721b9d871f449cdc49b3674b6eba7c53a45 100644 (file)
@@ -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>
index a3fe6f5b49bbdf7ad22a561aada28dcbb166ef92..f3730f2b27643f02cf5a7362d2d3292b9222c51e 100644 (file)
@@ -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;
 
 .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);
 }
 
 }
 
 .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;
 }
index 5ea8a0f915da9d7fa182654c960dc4cf64630add..463c4dc5d73d1194de486b913ffd47a49bc8c219 100644 (file)
@@ -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>
index 822d42b58f17495282a91b59299227a084c496ab..82e8d8d4faefcf99babe7148119a63a4328fe048 100644 (file)
@@ -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 }} />
index 86b9f83f7709ef529ad98db58dd4eb4913c5f25d..6541c6735390634b7ee263fd3f2d10034d86e4ac 100644 (file)
@@ -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>
index 62f7314addc128c882087648d65240ac66eb5bbe..58878f990c18c016b2415e9a79ff3ab2825b7ba5 100644 (file)
   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,
index 39c48ca9fc9e90534d285cd62eba4604c5f2589d..2bfdbb7415b200003eee0d1e906ef1fa0ae8a31c 100644 (file)
@@ -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 {
diff --git a/server/sonar-web/src/main/js/app/styles/components/search.css b/server/sonar-web/src/main/js/app/styles/components/search.css
deleted file mode 100644 (file)
index 2eed126..0000000
+++ /dev/null
@@ -1,65 +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.
- */
-.search-box {
-  position: relative;
-  font-size: 0;
-  white-space: nowrap;
-}
-
-.search-box-input {
-  vertical-align: middle;
-  width: 250px;
-  border: none !important;
-  font-size: var(--baseFontSize);
-}
-
-.search-box-input ~ .note {
-  opacity: 0;
-  transition: opacity 0.3s ease;
-}
-
-.search-box-input.touched ~ .note {
-  opacity: 1;
-}
-
-.search-box-submit {
-  display: inline-block;
-  vertical-align: middle;
-}
-
-.search-box-submit .icon-search:before {
-  color: var(--secondFontColor);
-  font-size: var(--mediumFontSize);
-}
-
-.search-box-submit .icon-search-new {
-  position: relative;
-  top: 1px;
-}
-
-.search-box-input-note {
-  position: absolute;
-  top: 100%;
-  left: 0;
-  line-height: 1;
-  color: var(--secondFontColor);
-  font-size: var(--smallFontSize);
-  white-space: nowrap;
-}
index 82baae3dac531376da35ddf274f619c93a5115bd..315803eb185966993195ff53916ee44a871220aa 100644 (file)
@@ -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);
index bccf053cfe775f90ef19afb6717ebfa0cc63d5d7..6f0a00b31e5fdca3e5fc0e5933f9f319a752b1aa 100644 (file)
@@ -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';
 
index c6c69e6757fe9ed8d1338ed74b2e747a22329ece..5764615be3275a0d9f2a9dc5bd234a278c470e77 100644 (file)
@@ -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();
index 7430714610985e30a6b82d69b78d99fe04344020..6f421e50e8800c824c12ada34ed2912cbad4625d 100644 (file)
@@ -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 {
index d8bf4203b0c19edb953268e10fd2b42244fc662a..023ab973177be99bff8215c3c5e9504a85fad09e 100644 (file)
@@ -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>{' '}
index 2de8ede896b4a20a826d5c148b057d2466bfdeb6..66658e61943d2630b264d6e71f1818a71934b76b 100644 (file)
   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;
index 3f254bf87ecdbb6f531ebfa60caba160c0818c54..3e666e19317d9c0a86cdca2c49eb375ae823580e 100644 (file)
 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
index 3198f422809d57b07a6ac7f8b57974d9f8c859ef..3ca0ceb35386bd9c2b96b9178a61fcd95f097487 100644 (file)
  * 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() {
index 5cafe6d1cd782e8ef4841fd8dddd94783f278a70..c8f3d84d5b11a1e19324ea235f585db1e7ef0e42 100644 (file)
@@ -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>
index 16525d55ee548cb04329e76bd023c02364e3ed22..a8a10e4a2c2ea109ef4575ce05570d59ee2b3616 100644 (file)
@@ -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() {
index 9acd7109e5739688c3b32021064e568d366ee25c..1c8401913bd087320157ccc9533b13032189369a 100644 (file)
@@ -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();
   }
 });
index 013f7cba90abcce1568cf8206e2ee5c73aa8a803..e0d8614362f748d39212feda64dd2e2137927095 100644 (file)
@@ -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>
index e75f10633a4f9d56a9dbeef3ce35dcf1d31f4c5f..33675c7bbb4b97e2e6e8d80a60acd656450306b2 100644 (file)
@@ -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>
index 3bef8189711ee051d117623439e1918614ec8ea3..1eaef17728ee8bbd43e032cb50f76242c0906b46 100644 (file)
@@ -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>
     );
   }
index 8303d70062387608f2e3375efc6cf52b5f1345e1..9737191977352c83d9bb1bbfe5c69ab07543aa60 100644 (file)
@@ -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>
+  );
 }
index 4dce97f4aef92946d5d58dad32cc5c4d35c836aa..42461a5120c93ed1213b23bab3e2bcd33c90f462 100644 (file)
@@ -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>
 `;
index 76e25c9c1f11c82eca45adbaa11f1f48fb021cc6..507aa1a836889c3cdd7caa174493619f8d1f387c 100644 (file)
@@ -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 });
     });
   };
 
index 82260039e0ef74e1a4647fb3c700f6db76f86104..c54dfd00607352a323010342c9a42ca0f413205f 100644 (file)
@@ -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 => {
index 639097378a0bc748752e9552f22b3538219e297a..82dfc9e1b67c59b61c560491000dacd97f5d5151 100644 (file)
@@ -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 */) => (
index 4ab52668b98e0208e6c568d2e96db6e546aab3af..69f811a5f452f6d4ac9ab96ceebbf01ea0d8060e 100644 (file)
@@ -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);
     }
   };
 
index 0fdb556893166e6b052b3bc8cce4f27adfaa5546..0115c49a68818a1e7c3666b395c63ddd74cb2b5b 100644 (file)
 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>
+  );
 }
index 264cb863e52477264bf97f803474a59a2fd5c543..c8de3ad390611adcfb977cca6a5f70d666173c45 100644 (file)
@@ -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}
index f5561258330cba159367134e0c6faa3fbc98bd30..71fc671f68ddaf54b2cef78f0d386b5bc51d037a 100644 (file)
@@ -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",
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/SearchFilter.tsx
deleted file mode 100644 (file)
index 5bc0764..0000000
+++ /dev/null
@@ -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>
-    );
-  }
-}
index b17ca9a1a21b35193b8185ec8d317868e305fd12..4934f7ecaf31414a68136ee3293d939aa9c8c0dd 100644 (file)
@@ -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>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/SearchFilter-test.tsx
deleted file mode 100644 (file)
index 2724f83..0000000
+++ /dev/null
@@ -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' });
-});
index 7825caf1ca2e4c4e7bd1ebe12a210155a7343b08..3154a9488fd288243d478e5143dfab0887998b3c 100644 (file)
@@ -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' } });
 });
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/__snapshots__/SearchFilter-test.tsx.snap
deleted file mode 100644 (file)
index 86e0c76..0000000
+++ /dev/null
@@ -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>
-`;
index d3d2ac3eb74d8fd1944632fbc428398fda190c02..870a9ad499a670affe57d8d5971bf62b4f6dac23 100644 (file)
@@ -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>
 `;
index 847569871691f5a5cc59bd08f8e081f46a23ecfb..1bd7e2865482214f11d0e0af5f5125c19817ab3f 100644 (file)
 .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 {
index 1d799214c95035a64361e2330a1809770b86855f..67c82b0da6a5ba3c9415591a8d6b453ac548a2e8 100644 (file)
@@ -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
index 1983d99dce63867d1ea1d696a920405ada373341..824c6ab2a5e38863197022c2e6682ffa05f755be 100644 (file)
@@ -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');
 });
 
index 25eae9b2ece204b31ef86210637b62cb46d3f0c5..254a3215e079e68d8c5bc9d2ced9dd6b53f871f5 100644 (file)
@@ -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"
index 70b309109d0d30042bdfbe9b2a944425dde9f6ef..3e6bbd28b32e612b456e4db2a9c509721990a6b5 100644 (file)
@@ -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 = {
diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersSearch.js b/server/sonar-web/src/main/js/apps/users/components/UsersSearch.js
deleted file mode 100644 (file)
index 32916a9..0000000
+++ /dev/null
@@ -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>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js b/server/sonar-web/src/main/js/apps/users/components/__tests__/UserSearch-test.js
deleted file mode 100644 (file)
index 71fd3cc..0000000
+++ /dev/null
@@ -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();
-});
diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UserSearch-test.js.snap
deleted file mode 100644 (file)
index de9d031..0000000
+++ /dev/null
@@ -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>
-`;
index a0f52d20cacd50c07adf637f03e6ad6caf868414..f7afc9e81aa1df82d789ed507f1ee2251f289c58 100644 (file)
@@ -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();
   }
 });
index 4b879a050badf24efb3833bf2d8983fe48903eaf..a95ed5eb718254f0b6b6a4cfb5c1b6d67923f68d 100644 (file)
@@ -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>
index ab64ad6167edc9d875ebecb44029f16b62840a01..fbf984e8743dc084d3d5ae501639f1d69c85bb71 100644 (file)
  * 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>
+  );
 }
index 2e4b787f7feb030f70b89e70a5a7e3ebcda665aa..5980b970470be798ac34eae088227845242a073a 100644 (file)
@@ -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
index 8fd5944e13e0fa2810d7de18cc68c3b0f5e13292..c99b781ab07b96b34b157e3ab8fefb1541331491 100644 (file)
@@ -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;
index 97339623edb6f9e1521137a9f56c7195b32e10cc..1b970113d5989f5c104a6eb9c48a8276b48d0c6e 100644 (file)
@@ -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();
index 95b602d96afad14b31a99936e1f99efb6f0a1872..fe9379484eaa32820eab9a13cb6f25beeb8a8bc4 100644 (file)
@@ -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">
index 9bfed621cc0ad3787f3e06caf8427a2ec48b527b..30b1761bac679c7c4c58bd3a2af4ab70855c6f07 100644 (file)
@@ -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">
index fe5e7370ea0a281657747a12408182a5b4caa96c..ed9fb2415684eeb29b2e2c98925ba8354944c210 100644 (file)
@@ -26,7 +26,8 @@ const props = {
   elements: [],
   onSearch: () => {},
   onSelect: () => {},
-  onUnselect: () => {}
+  onUnselect: () => {},
+  placeholder: ''
 };
 
 const elements = ['foo', 'bar', 'baz'];
index dd5d323ec6d015057adb608f2a6dd074d89c6d24..5af710f8bb1c928521163fc99c504c6561ddaf17 100644 (file)
@@ -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>
diff --git a/server/sonar-web/src/main/js/components/controls/SearchBox.css b/server/sonar-web/src/main/js/components/controls/SearchBox.css
new file mode 100644 (file)
index 0000000..64951c5
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+.search-box {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle;
+  font-size: 0;
+  white-space: nowrap;
+}
+
+.search-box,
+.search-box-input {
+  width: 100%;
+  max-width: 300px;
+}
+
+.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::placeholder {
+  color: var(--secondFontColor);
+  opacity: 1;
+}
+
+.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-input::-ms-clear,
+.search-box-input::-ms-reveal {
+  display: none;
+  width: 0;
+  height: 0;
+}
+
+.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 {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  line-height: 1;
+  color: var(--secondFontColor);
+  font-size: var(--smallFontSize);
+  white-space: nowrap;
+}
diff --git a/server/sonar-web/src/main/js/components/controls/SearchBox.tsx b/server/sonar-web/src/main/js/components/controls/SearchBox.tsx
new file mode 100644 (file)
index 0000000..08ebca7
--- /dev/null
@@ -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>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/SearchBox-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/SearchBox-test.tsx
new file mode 100644 (file)
index 0000000..b029de1
--- /dev/null
@@ -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();
+});
diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/SearchBox-test.tsx.snap
new file mode 100644 (file)
index 0000000..d69e12d
--- /dev/null
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/components/icons-components/SearchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/SearchIcon.tsx
new file mode 100644 (file)
index 0000000..ad4d513
--- /dev/null
@@ -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>
+  );
+}
index 365ed241de187ad033662c080b89682896382340..d25651d6c6c48639dcc7850c829374d7974db074 100644 (file)
  */
 // @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
index a9ac6e5d32847f432f2ec8873d5d5ee1bee9a888..a9a3617af05ff8613ddedeaf998c2cd837a81e08 100644 (file)
@@ -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}
index 979f6e3aeeff2b18f2f4e93ddc2b853d4ead3fd3..2db1235e5f1dc02f58927a6a1530b80f59dfedab 100644 (file)
@@ -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, '');
 }
index 6ef4a86dd89c9c964ada4e731cb0e2fbdfaf9286..395e8fd104f3bf426f08ce52e468450f93411d34 100644 (file)
@@ -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');
 });
index 9afc27fbd7b36523ab1fa591f45e5facb5b1ee2c..e2d57569b312d97857801abe911bbafa9a514129 100644 (file)
@@ -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]}
   />
index e4cd085eb059dd47157240c0cdcb85e76bb593a6..0e65602d0127a733d875329340ee858684b79c51 100644 (file)
@@ -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')}
index 01b046039dc881d6839e9b4aa0b36118688369fb..8665b925032cd00591b5f07b21775a71c8a3ef3c 100644 (file)
@@ -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> {
index 798f670b52c144d8daa1588bb4308a91a765fffc..cc52ffe23520d70c43e0919dbc87849b590101f1 100644 (file)
@@ -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...
 
 
 #------------------------------------------------------------------------------
index b85f3a79566f736d8bfc219c320bfcf5a6bc333a..72d0d27705c544e86ebe3064b7932f1960364bb8 100644 (file)
@@ -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);
diff --git a/tests/src/test/java/org/sonarqube/pageobjects/ProjectCodePage.java b/tests/src/test/java/org/sonarqube/pageobjects/ProjectCodePage.java
new file mode 100644 (file)
index 0000000..466ba60
--- /dev/null
@@ -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;
+  }
+}
index a7d994be3975a76940c0383f7eadcbaefa6d68c8..a6e6081438bf3e0352250349d3c4d05575d43552 100644 (file)
@@ -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;
   }
 
index eb6b6bb639e069f1503022d17afcb898e26a42d7..a3a47279ed7c0159f9f810bd0fa5d3bbc9c0602a 100644 (file)
 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);
   }
 }
index c0cc2a71e9e76cac03f075c2b0d17f8183cd0409..bf087c2917d5394567871a77da86adab64d4f0a3 100644 (file)
 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);
+  }
 }
index b79114be2ac4e3f5fd3ff8aca09dd62d80971e86..9318569d05f6397984d5eb4ff61a478d472f4b6a 100644 (file)
@@ -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");
+  }
 }
diff --git a/tests/src/test/resources/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html b/tests/src/test/resources/projectAdministration/ProjectBulkDeletionPageTest/bulk-delete-filter-projects.html
deleted file mode 100644 (file)
index 1b1ee44..0000000
+++ /dev/null
@@ -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>
diff --git a/tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html b/tests/src/test/resources/sourceCode/ProjectCodeTest/code_page_should_expand_root_dir.html
deleted file mode 100644 (file)
index c9737d5..0000000
+++ /dev/null
@@ -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>
diff --git a/tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html b/tests/src/test/resources/sourceCode/ProjectCodeTest/permalink.html
deleted file mode 100644 (file)
index 69364a6..0000000
+++ /dev/null
@@ -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>
diff --git a/tests/src/test/resources/sourceCode/ProjectCodeTest/search.html b/tests/src/test/resources/sourceCode/ProjectCodeTest/search.html
deleted file mode 100644 (file)
index 1594ee2..0000000
+++ /dev/null
@@ -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>
diff --git a/tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html b/tests/src/test/resources/sourceCode/ProjectCodeTest/test_project_code_page.html
deleted file mode 100644 (file)
index 9c6b163..0000000
+++ /dev/null
@@ -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>