executeBuild("shared/xoo-sample", "project-for-code", "Project For Code");
Selenese selenese = Selenese.builder().setHtmlTestsInClasspath("test_project_code_page",
- "/sourceCode/ProjectCodeTest/test_project_code_page.html"
+ "/sourceCode/ProjectCodeTest/test_project_code_page.html",
+ "/sourceCode/ProjectCodeTest/search.html",
+ "/sourceCode/ProjectCodeTest/permalink.html"
).build();
new SeleneseTest(selenese).runOn(orchestrator);
}
<tr>
<td>waitForText</td>
<td>css=#content</td>
- <td>*Hello.xoo*</td>
+ <td>*Hello.xoo*src/main/xoo/sample*</td>
</tr>
</tbody>
</table>
--- /dev/null
+<?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&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>
--- /dev/null
+<?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>
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2016 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 _ from 'underscore';
-import { pushPath, replacePath } from 'redux-simple-router';
-
-import { getChildren, getComponent, getTree, getBreadcrumbs } from '../../../api/components';
-import { translate } from '../../../helpers/l10n';
-import { getComponentUrl } from '../../../helpers/urls';
-
-const METRICS = [
- 'ncloc',
- 'code_smells',
- 'bugs',
- 'vulnerabilities',
- 'duplicated_lines_density',
- 'alert_status'
-];
-
-const METRICS_WITH_COVERAGE = [
- ...METRICS,
- 'coverage',
- 'it_coverage',
- 'overall_coverage'
-];
-
-const PAGE_SIZE = 100;
-
-export const INIT = 'INIT';
-export const BROWSE = 'BROWSE';
-export const LOAD_MORE = 'LOAD_MORE';
-export const SEARCH = 'SEARCH';
-export const SELECT_NEXT = 'SELECT_NEXT';
-export const SELECT_PREV = 'SELECT_PREV';
-export const UPDATE_QUERY = 'UPDATE_QUERY';
-export const START_FETCHING = 'START_FETCHING';
-export const STOP_FETCHING = 'STOP_FETCHING';
-export const RAISE_ERROR = 'RAISE_ERROR';
-
-export function initComponentAction (component, breadcrumbs = []) {
- return {
- type: INIT,
- component,
- breadcrumbs
- };
-}
-
-export function browseAction (component, children = [], breadcrumbs = [], total = 0) {
- return {
- type: BROWSE,
- component,
- children,
- breadcrumbs,
- total
- };
-}
-
-export function loadMoreAction (children, page) {
- return {
- type: LOAD_MORE,
- children,
- page
- };
-}
-
-export function searchAction (components) {
- return {
- type: SEARCH,
- components
- };
-}
-
-export function updateQueryAction (query) {
- return {
- type: UPDATE_QUERY,
- query
- };
-}
-
-export function selectNext () {
- return { type: SELECT_NEXT };
-}
-
-export function selectPrev () {
- return { type: SELECT_PREV };
-}
-
-export function startFetching () {
- return { type: START_FETCHING };
-}
-
-export function stopFetching () {
- return { type: STOP_FETCHING };
-}
-
-export function raiseError (message) {
- return {
- type: RAISE_ERROR,
- message
- };
-}
-
-function getPath (componentKey) {
- return '/' + encodeURIComponent(componentKey);
-}
-
-function expandRootDir ({ children, total, ...other }) {
- const rootDir = children.find(component => component.qualifier === 'DIR' && component.name === '/');
- if (rootDir) {
- return getChildren(rootDir.key, METRICS_WITH_COVERAGE).then(r => {
- const nextChildren = _.without([...children, ...r.components], rootDir);
- const nextTotal = total + r.components.length - /* root dir */ 1;
- return { children: nextChildren, total: nextTotal, ...other };
- });
- } else {
- return { children, total, ...other };
- }
-}
-
-function prepareChildren (r) {
- return { children: r.components, total: r.paging.total, page: r.paging.pageIndex };
-}
-
-function skipRootDir (breadcrumbs) {
- return breadcrumbs.filter(component => {
- return !(component.qualifier === 'DIR' && component.name === '/');
- });
-}
-
-function retrieveComponentBase (componentKey, candidate) {
- return candidate ?
- Promise.resolve(candidate) :
- getComponent(componentKey, METRICS_WITH_COVERAGE);
-}
-
-function retrieveComponentChildren (componentKey, candidate) {
- return candidate && candidate.children ?
- Promise.resolve({ children: candidate.children, total: candidate.total }) :
- getChildren(componentKey, METRICS_WITH_COVERAGE, { ps: PAGE_SIZE }).then(prepareChildren).then(expandRootDir);
-}
-
-function retrieveComponentBreadcrumbs (componentKey, candidate) {
- return candidate && candidate.breadcrumbs ?
- Promise.resolve(candidate.breadcrumbs) :
- getBreadcrumbs({ key: componentKey }).then(skipRootDir);
-}
-
-function retrieveComponent (componentKey, bucket) {
- const candidate = _.findWhere(bucket, { key: componentKey });
- return Promise.all([
- retrieveComponentBase(componentKey, candidate),
- retrieveComponentChildren(componentKey, candidate),
- retrieveComponentBreadcrumbs(componentKey, candidate)
- ]);
-}
-
-function requestTree (query, baseComponent, dispatch) {
- dispatch(startFetching());
- return getTree(baseComponent.key, { q: query, s: 'qualifier,name' })
- .then(r => dispatch(searchAction(r.components)))
- .then(() => dispatch(stopFetching()));
-}
-
-async function getErrorMessage (response) {
- switch (response.status) {
- case 401:
- return translate('not_authorized');
- default:
- try {
- const json = await response.json();
- return json['err_msg'] ||
- (json.errors && _.pluck(json.errors, 'msg').join('. ')) ||
- translate('default_error_message');
- } catch (e) {
- return translate('default_error_message');
- }
- }
-}
-
-export function initComponent (componentKey, breadcrumbs) {
- return dispatch => {
- dispatch(startFetching());
- return getComponent(componentKey, METRICS_WITH_COVERAGE)
- .then(component => dispatch(initComponentAction(component, breadcrumbs)))
- .then(() => dispatch(replacePath(getPath(componentKey))))
- .then(() => dispatch(stopFetching()));
- };
-}
-
-export function browse (componentKey) {
- return (dispatch, getState) => {
- const { bucket } = getState();
- dispatch(startFetching());
- return retrieveComponent(componentKey, bucket)
- .then(([component, children, breadcrumbs]) => {
- if (component.refKey) {
- window.location = getComponentUrl(component.refKey);
- return new Promise();
- } else {
- dispatch(browseAction(component, children.children, breadcrumbs, children.total));
- }
- })
- .then(() => dispatch(pushPath(getPath(componentKey))))
- .then(() => dispatch(stopFetching()))
- .catch(e => {
- getErrorMessage(e.response)
- .then(message => dispatch(raiseError(message)));
- });
- };
-}
-
-export function loadMore () {
- return (dispatch, getState) => {
- const { baseComponent, page } = getState().current;
- return getChildren(baseComponent.key, METRICS_WITH_COVERAGE, { p: page + 1, ps: PAGE_SIZE })
- .then(prepareChildren)
- .then(({ children }) => {
- dispatch(loadMoreAction(children, page + 1));
- dispatch(stopFetching());
- })
- .catch(e => {
- getErrorMessage(e.response)
- .then(message => dispatch(raiseError(message)));
- });
- };
-}
-
-let debouncedSearch = function (query, baseComponent, dispatch) {
- if (query) {
- requestTree(query, baseComponent, dispatch);
- } else {
- dispatch(searchAction(null));
- }
-};
-debouncedSearch = _.debounce(debouncedSearch, 250);
-
-export function search (query, baseComponent) {
- return dispatch => {
- dispatch(updateQueryAction(query));
-
- if (query.length > 2 || !query.length) {
- debouncedSearch(query, baseComponent, dispatch);
- }
- };
-}
-
-export function selectCurrent () {
- return (dispatch, getState) => {
- const { searchResults } = getState().current;
- if (searchResults) {
- const componentKey = getState().current.searchSelectedItem.key;
- dispatch(browse(componentKey));
- }
- };
-}
*/
import React from 'react';
import { render } from 'react-dom';
-import { Provider } from 'react-redux';
-import { Router, Route, useRouterHistory } from 'react-router';
-import { createHashHistory } from 'history';
-import { syncReduxAndRouter } from 'redux-simple-router';
+import { Router, Route, Redirect, useRouterHistory } from 'react-router';
+import { createHistory } from 'history';
-import Code from './components/Code';
-import configureStore from './store/configureStore';
+import App from './components/App';
-import './styles/code.css';
+window.sonarqube.appStarted.then(options => {
+ const el = document.querySelector(options.el);
-const store = configureStore();
-const history = useRouterHistory(createHashHistory)({ queryKey: false });
+ const history = useRouterHistory(createHistory)({
+ basename: window.baseUrl + '/code'
+ });
-syncReduxAndRouter(history, store);
-
-window.sonarqube.appStarted.then(({ el, component }) => {
- const CodeWithComponent = () => {
- return <Code component={component}/>;
+ const AppWithComponent = (props) => {
+ return <App {...props} component={options.component}/>;
};
- render(
- <Provider store={store}>
- <Router history={history}>
- <Route path="/" component={CodeWithComponent}/>
- <Route path="/:path" component={CodeWithComponent}/>
- </Router>
- </Provider>,
- document.querySelector(el));
+ render((
+ <Router history={history}>
+ <Redirect from="/index" to="/"/>
+ <Route path="/" component={AppWithComponent}/>
+ </Router>
+ ), el);
});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 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.
+ */
+const bucket = {};
+const childrenBucket = {};
+const breadcrumbsBucket = {};
+
+export function addComponent (component) {
+ bucket[component.key] = component;
+}
+
+export function getComponent (componentKey) {
+ return bucket[componentKey];
+}
+
+export function addComponentChildren (componentKey, children, total) {
+ childrenBucket[componentKey] = { children, total };
+}
+
+export function getComponentChildren (componentKey) {
+ return childrenBucket[componentKey];
+}
+
+export function addComponentBreadcrumbs (componentKey, breadcrumbs) {
+ breadcrumbsBucket[componentKey] = breadcrumbs;
+}
+
+export function getComponentBreadcrumbs (componentKey) {
+ return breadcrumbsBucket[componentKey];
+}
--- /dev/null
+.code-breadcrumbs {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.code-breadcrumbs > li {
+ padding: 5px 5px 3px;
+}
+
+.code-breadcrumbs > li:first-child {
+ padding-left: 0;
+}
+
+.code-breadcrumbs > li::after {
+ position: relative;
+ top: -1px;
+ padding-left: 10px;
+ color: #777;
+ font-size: 11px;
+ content: ">";
+}
+
+.code-breadcrumbs > li:last-child::after {
+ display: none;
+}
+
+.code-components-cell {
+ min-width: 80px;
+ padding-left: 30px !important;
+ box-sizing: border-box;
+}
+
+.code-truncated {
+ display: inline-block;
+ vertical-align: text-top;
+ max-width: 50vw;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.code-name-cell {
+ max-width: 0;
+}
+
+.code-name-cell .code-truncated {
+ max-width: 100%;
+}
+
+.code-search {
+ margin-bottom: 10px;
+}
+
+.code-search-with-results + .code-components {
+ 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;
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 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 classNames from 'classnames';
+import React from 'react';
+
+import Components from './Components';
+import Breadcrumbs from './Breadcrumbs';
+import SourceViewer from './../../../components/source-viewer/SourceViewer';
+import Search from './Search';
+import ListFooter from '../../../components/shared/list-footer';
+import { retrieveComponentBase, retrieveComponent, loadMoreChildren, parseError } from '../utils';
+import { addComponentBreadcrumbs } from '../bucket';
+import { selectCoverageMetric } from '../../../helpers/measures';
+
+import '../code.css';
+
+export default class App extends React.Component {
+ state = {
+ loading: true,
+ baseComponent: null,
+ components: null,
+ breadcrumbs: [],
+ total: 0,
+ page: 0,
+ sourceViewer: null,
+ error: null
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ this.handleComponentChange();
+ }
+
+ componentDidUpdate (prevProps) {
+ if (prevProps.component !== this.props.component) {
+ this.handleComponentChange();
+ } else if (prevProps.location !== this.props.location) {
+ this.handleUpdate();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ handleComponentChange () {
+ const { component } = this.props;
+
+ // we already know component's breadcrumbs,
+ addComponentBreadcrumbs(component.key, component.breadcrumbs);
+
+ this.setState({ loading: true });
+ retrieveComponentBase(component.key).then(component => {
+ const prefix = selectCoverageMetric(component.measures);
+ this.coverageMetric = `${prefix}coverage`;
+ this.handleUpdate();
+ }).catch(e => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ parseError(e).then(this.handleError.bind(this));
+ }
+ });
+ }
+
+ loadComponent (componentKey) {
+ this.setState({ loading: true });
+
+ retrieveComponent(componentKey).then(r => {
+ if (this.mounted) {
+ if (['FIL', 'UTS'].includes(r.component.qualifier)) {
+ this.setState({
+ loading: false,
+ sourceViewer: r.component,
+ breadcrumbs: r.breadcrumbs,
+ searchResults: null
+ });
+ } else {
+ this.setState({
+ loading: false,
+ baseComponent: r.component,
+ components: r.components,
+ breadcrumbs: r.breadcrumbs,
+ total: r.total,
+ page: r.page,
+ sourceViewer: null,
+ searchResults: null
+ });
+ }
+ }
+ }).catch(e => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ parseError(e).then(this.handleError.bind(this));
+ }
+ });
+ }
+
+ handleUpdate () {
+ const { component, location } = this.props;
+ const { selected } = location.query;
+ const finalKey = selected || component.key;
+
+ this.loadComponent(finalKey);
+ }
+
+ handleLoadMore () {
+ const { baseComponent, page } = this.state;
+ loadMoreChildren(baseComponent.key, page + 1).then(r => {
+ if (this.mounted) {
+ this.setState({
+ components: [...this.state.components, ...r.components],
+ page: r.page,
+ total: r.total
+ });
+ }
+ }).catch(e => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ parseError(e).then(this.handleError.bind(this));
+ }
+ });
+ }
+
+ handleError (error) {
+ if (this.mounted) {
+ this.setState({ error });
+ }
+ }
+
+ render () {
+ const { component, location } = this.props;
+ const {
+ loading,
+ error,
+ baseComponent,
+ components,
+ breadcrumbs,
+ total,
+ sourceViewer
+ } = this.state;
+
+ const shouldShowSourceViewer = !!sourceViewer;
+ const shouldShowComponents = !shouldShowSourceViewer && components;
+ const shouldShowBreadcrumbs = Array.isArray(breadcrumbs) && breadcrumbs.length > 1;
+
+ const componentsClassName = classNames('spacer-top', { 'new-loading': loading });
+
+ return (
+ <div className="page page-limited">
+ {error && (
+ <div className="alert alert-danger">
+ {error}
+ </div>
+ )}
+
+ <Search
+ location={location}
+ component={component}
+ onError={this.handleError.bind(this)}/>
+
+
+ <div className="code-components">
+ {shouldShowBreadcrumbs && (
+ <Breadcrumbs
+ rootComponent={component}
+ breadcrumbs={breadcrumbs}/>
+ )}
+
+ {shouldShowComponents && (
+ <div className={componentsClassName}>
+ <Components
+ rootComponent={component}
+ baseComponent={baseComponent}
+ components={components}
+ coverageMetric={this.coverageMetric}/>
+ </div>
+ )}
+
+ {shouldShowComponents && (
+ <ListFooter
+ count={components.length}
+ total={total}
+ loadMore={this.handleLoadMore.bind(this)}/>
+ )}
+
+ {shouldShowSourceViewer && (
+ <div className="spacer-top">
+ <SourceViewer component={sourceViewer}/>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ }
+}
import ComponentName from './ComponentName';
-const Breadcrumb = ({ component, onBrowse }) => (
+const Breadcrumb = ({ rootComponent, component, canBrowse }) => (
<ComponentName
+ rootComponent={rootComponent}
component={component}
- onBrowse={onBrowse}/>
+ canBrowse={canBrowse}/>
);
export default Breadcrumb;
import Breadcrumb from './Breadcrumb';
-const Breadcrumbs = ({ breadcrumbs, onBrowse }) => (
+const Breadcrumbs = ({ rootComponent, breadcrumbs }) => (
<ul className="code-breadcrumbs">
{breadcrumbs.map((component, index) => (
<li key={component.key}>
<Breadcrumb
+ rootComponent={rootComponent}
component={component}
- onBrowse={index + 1 < breadcrumbs.length ? onBrowse : null}/>
+ canBrowse={index < breadcrumbs.length - 1}/>
</li>
))}
</ul>
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2016 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 classNames from 'classnames';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-
-import Components from './Components';
-import Breadcrumbs from './Breadcrumbs';
-import SourceViewer from './SourceViewer';
-import Search from './Search';
-import ListFooter from '../../../components/shared/list-footer';
-import { initComponent, browse, loadMore } from '../actions';
-
-class Code extends Component {
- componentDidMount () {
- const { dispatch, component, routing } = this.props;
- const selectedKey = (routing.path && decodeURIComponent(routing.path.substr(1))) || component.key;
- dispatch(initComponent(component.key, component.breadcrumbs))
- .then(() => dispatch(browse(selectedKey)));
- }
-
- componentWillReceiveProps (nextProps) {
- if (nextProps.routing !== this.props.routing) {
- const { dispatch, routing, component, fetching } = nextProps;
- if (!fetching) {
- const selectedKey = (routing.path && decodeURIComponent(routing.path.substr(1))) || component.key;
- dispatch(browse(selectedKey));
- }
- }
- }
-
- handleBrowse (component) {
- const { dispatch } = this.props;
- dispatch(browse(component.key));
- }
-
- handleLoadMore () {
- const { dispatch } = this.props;
- dispatch(loadMore());
- }
-
- render () {
- const {
- fetching,
- baseComponent,
- components,
- breadcrumbs,
- sourceViewer,
- coverageMetric,
- searchResults,
- errorMessage,
- total
- } = this.props;
- const shouldShowSearchResults = !!searchResults;
- const shouldShowSourceViewer = !!sourceViewer;
- const shouldShowComponents = !shouldShowSearchResults && !shouldShowSourceViewer && components;
- const shouldShowBreadcrumbs = !shouldShowSearchResults && Array.isArray(breadcrumbs) && breadcrumbs.length > 1;
-
- const componentsClassName = classNames('spacer-top', { 'new-loading': fetching });
-
- return (
- <div className="page page-limited">
- <header className="page-header">
- <Search component={this.props.component}/>
-
- <div
- className="pull-left"
- style={{ visibility: fetching ? 'visible' : 'hidden' }}>
- <i className="spinner"/>
- </div>
- </header>
-
- {errorMessage && (
- <div className="alert alert-danger">
- {errorMessage}
- </div>
- )}
-
- {shouldShowBreadcrumbs && (
- <Breadcrumbs
- breadcrumbs={breadcrumbs}
- onBrowse={this.handleBrowse.bind(this)}/>
- )}
-
- {shouldShowSearchResults && (
- <div className={componentsClassName}>
- <Components
- components={searchResults}
- onBrowse={this.handleBrowse.bind(this)}/>
- </div>
- )}
-
- {shouldShowComponents && (
- <div className={componentsClassName}>
- <Components
- baseComponent={baseComponent}
- components={components}
- coverageMetric={coverageMetric}
- onBrowse={this.handleBrowse.bind(this)}/>
- </div>
- )}
-
- {shouldShowComponents && (
- <ListFooter
- count={components.length}
- total={total}
- loadMore={this.handleLoadMore.bind(this)}/>
- )}
-
- {shouldShowSourceViewer && (
- <div className="spacer-top">
- <SourceViewer component={sourceViewer}/>
- </div>
- )}
- </div>
- );
- }
-}
-
-export default connect(state => {
- return {
- routing: state.routing,
- fetching: state.current.fetching,
- baseComponent: state.current.baseComponent,
- components: state.current.components,
- breadcrumbs: state.current.breadcrumbs,
- sourceViewer: state.current.sourceViewer,
- coverageMetric: state.current.coverageMetric,
- searchResults: state.current.searchResults,
- errorMessage: state.current.errorMessage,
- total: state.current.total
- };
-})(Code);
import classNames from 'classnames';
import React from 'react';
import ReactDOM from 'react-dom';
-import { connect } from 'react-redux';
+import shallowCompare from 'react-addons-shallow-compare';
import ComponentName from './ComponentName';
import ComponentMeasure from './ComponentMeasure';
const TOP_OFFSET = 200;
const BOTTOM_OFFSET = 10;
-class Component extends React.Component {
+export default class Component extends React.Component {
componentDidMount () {
this.handleUpdate();
}
+ shouldComponentUpdate (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+
componentDidUpdate () {
this.handleUpdate();
}
handleUpdate () {
const { selected } = this.props;
+
+ // scroll viewport so the current selected component is visible
if (selected) {
setTimeout(() => {
this.handleScroll();
}
render () {
- const { component, selected, previous, coverageMetric, onBrowse, isView } = this.props;
+ const { component, rootComponent, selected, previous, coverageMetric, canBrowse } = this.props;
+ const isView = ['VW', 'SVW'].includes(rootComponent.qualifier);
let componentAction = null;
)}
<ComponentName
component={component}
+ rootComponent={rootComponent}
previous={previous}
- onBrowse={onBrowse}/>
+ canBrowse={canBrowse}/>
</td>
<td className="thin nowrap text-right">
<div className="code-components-cell">
);
}
}
-
-function mapStateToProps (state, ownProps) {
- return {
- selected: state.current.searchSelectedItem === ownProps.component,
- isView: state.current.isView
- };
-}
-
-export default connect(mapStateToProps)(Component);
*/
import _ from 'underscore';
import React from 'react';
+import { Link } from 'react-router';
import Truncated from './Truncated';
import QualifierIcon from '../../../components/shared/qualifier-icon';
return prefix.substr(0, prefix.length - lastPrefixPart.length);
}
-const Component = ({ component, previous, onBrowse }) => {
- const handleClick = (e) => {
- e.preventDefault();
- onBrowse(component);
- };
-
+const Component = ({ component, rootComponent, previous, canBrowse }) => {
const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR';
const prefix = areBothDirs ? mostCommitPrefix([component.name + '/', previous.name + '/']) : '';
const name = prefix ? (
if (component.refKey) {
inner = <a href={getComponentUrl(component.refKey)}>{name}</a>;
} else {
- if (onBrowse) {
- inner = <a onClick={handleClick} href="#">{name}</a>;
+ if (canBrowse) {
+ const query = { id: rootComponent.key };
+ if (component.key !== rootComponent.key) {
+ Object.assign(query, { selected: component.key });
+ }
+ inner = (
+ <Link to={{ pathname: '/', query }}>
+ {name}
+ </Link>
+ );
} else {
inner = <span>{name}</span>;
}
import ComponentsEmpty from './ComponentsEmpty';
import ComponentsHeader from './ComponentsHeader';
-const Components = ({ baseComponent, components, coverageMetric, onBrowse }) => (
+const Components = ({ rootComponent, baseComponent, components, selected, coverageMetric }) => (
<table className="data zebra">
<ComponentsHeader baseComponent={baseComponent}/>
{baseComponent && (
<tbody>
<Component
key={baseComponent.key}
+ rootComponent={rootComponent}
component={baseComponent}
coverageMetric={coverageMetric}/>
<tr className="blank">
components.map((component, index, list) => (
<Component
key={component.key}
+ rootComponent={rootComponent}
component={component}
+ selected={component === selected}
previous={index > 0 ? list[index - 1] : null}
coverageMetric={coverageMetric}
- onBrowse={onBrowse}/>
+ canBrowse={true}/>
))
) : (
<ComponentsEmpty/>
<td colSpan="2">
{translate('no_results')}
</td>
- <td colSpan="5">
+ <td colSpan="6">
</td>
</tr>
* 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, { Component } from 'react';
-import { connect } from 'react-redux';
+import React from 'react';
+import shallowCompare from 'react-addons-shallow-compare';
import classNames from 'classnames';
+import debounce from 'lodash/debounce';
-import { search, selectCurrent, selectNext, selectPrev } from '../actions';
+import Components from './Components';
+import { getTree } from '../../../api/components';
import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { parseError } from '../utils';
+import { getComponentUrl } from '../../../helpers/urls';
+
+export default class Search extends React.Component {
+ static contextTypes = {
+ router: React.PropTypes.object.isRequired
+ };
+
+ static propTypes = {
+ component: React.PropTypes.object.isRequired,
+ location: React.PropTypes.object.isRequired,
+ onError: React.PropTypes.func.isRequired
+ };
+
+ state = {
+ query: '',
+ loading: false,
+ results: null,
+ selectedIndex: null
+ };
+
+ componentWillMount () {
+ this.handleSearch = debounce(this.handleSearch.bind(this), 250);
+ }
-class Search extends Component {
componentDidMount () {
+ this.mounted = true;
this.refs.input.focus();
}
+ componentWillReceiveProps (nextProps) {
+ // if the url has change, reset the current state
+ if (nextProps.location !== this.props.location) {
+ this.setState({
+ query: '',
+ loading: false,
+ results: null,
+ selectedIndex: null
+ });
+ }
+ }
+
+ shouldComponentUpdate (nextProps, nextState) {
+ return shallowCompare(this, nextProps, nextState);
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ checkInputValue (query) {
+ return this.refs.input.value === query;
+ }
+
+ handleSelectNext () {
+ const { selectedIndex, results } = this.state;
+ if (results != null && selectedIndex != null && selectedIndex < results.length - 1) {
+ this.setState({ selectedIndex: selectedIndex + 1 });
+ }
+ }
+
+ handleSelectPrevious () {
+ const { selectedIndex, results } = this.state;
+ if (results != null && selectedIndex != null && selectedIndex > 0) {
+ this.setState({ selectedIndex: selectedIndex - 1 });
+ }
+ }
+
+ handleSelectCurrent () {
+ const { component } = this.props;
+ const { results, selectedIndex } = this.state;
+ if (results != null && selectedIndex != null) {
+ const selected = results[selectedIndex];
+
+ if (selected.refKey) {
+ window.location = getComponentUrl(selected.refKey);
+ } else {
+ this.context.router.push({
+ pathname: '/',
+ query: {
+ id: component.key,
+ selected: selected.key
+ }
+ });
+ }
+ }
+ }
+
handleKeyDown (e) {
- const { dispatch } = this.props;
switch (e.keyCode) {
case 13:
e.preventDefault();
- dispatch(selectCurrent());
+ this.handleSelectCurrent();
break;
case 38:
e.preventDefault();
- dispatch(selectPrev());
+ this.handleSelectPrevious();
break;
case 40:
e.preventDefault();
- dispatch(selectNext());
+ this.handleSelectNext();
break;
- default:
+ default: // do nothing
+ }
+ }
+
+ handleSearch (query) {
+ // first time check if value has changed due to debounce
+ if (this.mounted && this.checkInputValue(query)) {
+ const { component, onError } = this.props;
+ this.setState({ loading: true });
+ getTree(component.key, { q: query, s: 'qualifier,name' })
+ .then(r => {
+ // second time check if value has change due to api request
+ if (this.mounted && this.checkInputValue(query)) {
+ this.setState({
+ results: r.components,
+ selectedIndex: r.components.length > 0 ? 0 : null,
+ loading: false
+ });
+ }
+ })
+ .catch(e => {
+ // second time check if value has change due to api request
+ if (this.mounted && this.checkInputValue(query)) {
+ this.setState({ loading: false });
+ parseError(e).then(onError);
+ }
+ });
+ }
+ }
- // do nothing
+ handleQueryChange (query) {
+ this.setState({ query });
+ if (query.length < 3) {
+ this.setState({ results: null });
+ } else {
+ this.handleSearch(query);
}
}
- handleSearch (e) {
+ handleInputChange (e) {
+ const query = e.target.value;
+ this.handleQueryChange(query);
+ }
+
+ handleSubmit (e) {
e.preventDefault();
- const { dispatch, component } = this.props;
const query = this.refs.input.value;
- dispatch(search(query, component));
+ this.handleQueryChange(query);
}
render () {
- const { query } = this.props;
+ const { component } = this.props;
+ const { query, loading, selectedIndex, results } = this.state;
+ const selected = selectedIndex != null && results != null ? results[selectedIndex] : null;
+ const containerClassName = classNames('code-search', {
+ 'code-search-with-results': results != null
+ });
const inputClassName = classNames('search-box-input', {
'touched': query.length > 0 && query.length < 3
});
return (
- <form
- onSubmit={this.handleSearch.bind(this)}
- className="search-box code-search-box">
- <button className="search-box-submit button-clean">
- <i className="icon-search"></i>
- </button>
- <input
- ref="input"
- onKeyDown={this.handleKeyDown.bind(this)}
- onChange={this.handleSearch.bind(this)}
- value={query}
- className={inputClassName}
- type="search"
- name="q"
- placeholder={translate('search_verb')}
- maxLength="100"
- autoComplete="off"/>
- <div className="note">
- {translateWithParameters('select2.tooShort', 3)}
- </div>
- </form>
+ <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"></i>
+ </button>
+
+ <input
+ ref="input"
+ 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>
+
+ {results != null && (
+ <Components
+ rootComponent={component}
+ components={results}
+ selected={selected}/>
+ )}
+ </div>
);
}
}
-
-export default connect(state => {
- return { query: state.current.searchQuery };
-})(Search);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2016 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 React, { Component } from 'react';
-
-import BaseSourceViewer from '../../../components/source-viewer/main';
-import { getPeriodDate, getPeriodLabel } from '../../../helpers/periods';
-
-export default class SourceViewer extends Component {
- componentDidMount () {
- this.renderSourceViewer();
- }
-
- shouldComponentUpdate (nextProps) {
- return nextProps.component.id !== this.props.component.id;
- }
-
- componentWillUpdate () {
- this.destroySourceViewer();
- }
-
- componentDidUpdate () {
- this.renderSourceViewer();
- }
-
- componentWillUnmount () {
- this.destroySourceViewer();
- }
-
- renderSourceViewer () {
- this.sourceViewer = new BaseSourceViewer();
- this.sourceViewer.render().$el.appendTo(this.refs.container);
- this.sourceViewer.open(this.props.component.id);
- this.sourceViewer.on('loaded', this.handleLoad.bind(this));
- }
-
- destroySourceViewer () {
- this.sourceViewer.destroy();
- }
-
- handleLoad () {
- const { period } = this.props;
-
- if (period) {
- const periodDate = getPeriodDate(period);
- const periodLabel = getPeriodLabel(period);
- this.sourceViewer.filterLinesByDate(periodDate, periodLabel);
- }
- }
-
- render () {
- return <div ref="container" className="code-source-viewer"></div>;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2016 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 _ from 'underscore';
-
-import {
- INIT,
- BROWSE,
- LOAD_MORE,
- SEARCH,
- UPDATE_QUERY,
- SELECT_NEXT,
- SELECT_PREV,
- START_FETCHING,
- STOP_FETCHING,
- RAISE_ERROR
-} from '../actions';
-
-function hasSourceCode (component) {
- return component.qualifier === 'FIL' || component.qualifier === 'UTS';
-}
-
-function selectCoverageMetric (component) {
- const coverage = _.findWhere(component.measures, { metric: 'coverage' });
- const itCoverage = _.findWhere(component.measures, { metric: 'it_coverage' });
- const overallCoverage = _.findWhere(component.measures, { metric: 'overall_coverage' });
-
- if (coverage != null && itCoverage != null && overallCoverage != null) {
- return 'overall_coverage';
- } else if (coverage != null) {
- return 'coverage';
- } else {
- return 'it_coverage';
- }
-}
-
-function merge (components, candidate) {
- const found = _.findWhere(components, { key: candidate.key });
- const newEntry = Object.assign({}, found, candidate);
- return [...(_.without(components, found)), newEntry];
-}
-
-function compare (a, b) {
- if (a === b) {
- return 0;
- }
- return a > b ? 1 : -1;
-}
-
-function sortChildren (children) {
- const QUALIFIERS_ORDER = ['FIL', 'UTS', 'DIR'];
- const temp = [...children];
- temp.sort((a, b) => {
- const qualifierA = QUALIFIERS_ORDER.indexOf(a.qualifier);
- const qualifierB = QUALIFIERS_ORDER.indexOf(b.qualifier);
- if (qualifierA !== qualifierB) {
- return compare(qualifierA, qualifierB);
- } else {
- return compare(a.name, b.name);
- }
- });
- return temp;
-}
-
-function getNext (element, list) {
- if (list) {
- const length = list.length;
- const index = list.indexOf(element);
- return index < length - 1 ? list[index + 1] : element;
- } else {
- return element;
- }
-}
-
-function getPrev (element, list) {
- if (list) {
- const index = list.indexOf(element);
- return index > 0 ? list[index - 1] : element;
- } else {
- return element;
- }
-}
-
-export const initialState = {
- fetching: false,
- baseComponent: null,
- components: null,
- breadcrumbs: null,
- sourceViewer: null,
- searchResults: null,
- searchQuery: '',
- searchSelectedItem: null,
- coverageMetric: null,
- isView: false,
- baseBreadcrumbs: [],
- errorMessage: null
-};
-
-export function current (state = initialState, action = {}) {
- /* eslint no-case-declarations: 0 */
- /* FIXME fix it ^^^ */
- switch (action.type) {
- case INIT:
- const coverageMetric = selectCoverageMetric(action.component);
- const baseBreadcrumbs = action.breadcrumbs.length > 1 ? _.initial(action.breadcrumbs) : [];
- const isView = action.component.qualifier === 'VW' || action.component.qualifier === 'SVW';
-
- return { ...state, coverageMetric, baseBreadcrumbs, isView };
- case BROWSE:
- const baseComponent = hasSourceCode(action.component) ? null : action.component;
- const components = hasSourceCode(action.component) ? null : sortChildren(action.children);
- const baseBreadcrumbsLength = state.baseBreadcrumbs.length;
- const breadcrumbs = action.breadcrumbs.slice(baseBreadcrumbsLength);
- const sourceViewer = hasSourceCode(action.component) ? action.component : null;
-
- return {
- ...state,
- baseComponent,
- components,
- breadcrumbs,
- sourceViewer,
- total: action.total,
- page: 1,
- searchResults: null,
- searchQuery: '',
- searchSelectedItem: null,
- errorMessage: null
- };
- case LOAD_MORE:
- return {
- ...state,
- components: sortChildren([...state.components, ...action.children]),
- page: action.page
- };
- case SEARCH:
- return {
- ...state,
- searchResults: action.components,
- searchSelectedItem: _.first(action.components),
- sourceViewer: null,
- errorMessage: null
- };
- case UPDATE_QUERY:
- return { ...state, searchQuery: action.query };
- case SELECT_NEXT:
- return {
- ...state,
- searchSelectedItem: getNext(state.searchSelectedItem, state.searchResults)
- };
- case SELECT_PREV:
- return {
- ...state,
- searchSelectedItem: getPrev(state.searchSelectedItem, state.searchResults)
- };
- case START_FETCHING:
- return { ...state, fetching: true };
- case STOP_FETCHING:
- return { ...state, fetching: false };
- case RAISE_ERROR:
- return {
- ...state,
- errorMessage: action.message,
- fetching: false
- };
- default:
- return state;
- }
-}
-
-export function bucket (state = [], action = {}) {
- switch (action.type) {
- case INIT:
- return merge(state, action.component);
- case BROWSE:
- const candidate = Object.assign({}, action.component, {
- children: action.children,
- total: action.total,
- breadcrumbs: action.breadcrumbs
- });
- const nextState = merge(state, candidate);
- return action.children.reduce((currentState, nextComponent) => {
- const nextComponentWidthBreadcrumbs = Object.assign({}, nextComponent, {
- breadcrumbs: [...action.breadcrumbs, nextComponent]
- });
- return merge(currentState, nextComponentWidthBreadcrumbs);
- }, nextState);
- default:
- return state;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2016 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 { createStore, applyMiddleware, combineReducers } from 'redux';
-import thunk from 'redux-thunk';
-import createLogger from 'redux-logger';
-import { routeReducer } from 'redux-simple-router';
-import { current, bucket } from '../reducers';
-
-const logger = createLogger({
- predicate: () => process.env.NODE_ENV !== 'production'
-});
-
-const createStoreWithMiddleware = applyMiddleware(
- thunk,
- logger
-)(createStore);
-
-const reducer = combineReducers({
- routing: routeReducer,
- current,
- bucket
-});
-
-export default function configureStore () {
- return createStoreWithMiddleware(reducer);
-}
+++ /dev/null
-.code-breadcrumbs {
- display: flex;
- flex-wrap: wrap;
-}
-
-.code-breadcrumbs > li {
- padding: 5px 5px 3px;
-}
-
-.code-breadcrumbs > li:first-child {
- padding-left: 0;
-}
-
-.code-breadcrumbs > li::after {
- position: relative;
- top: -1px;
- padding-left: 10px;
- color: #777;
- font-size: 11px;
- content: ">";
-}
-
-.code-breadcrumbs > li:last-child::after {
- display: none;
-}
-
-.code-components-cell {
- min-width: 80px;
- padding-left: 30px !important;
- box-sizing: border-box;
-}
-
-.code-truncated {
- display: inline-block;
- vertical-align: text-top;
- max-width: 50vw;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.code-name-cell {
- max-width: 0;
-}
-
-.code-name-cell .code-truncated {
- max-width: 100%;
-}
-
-.code-search-box {
- float: left;
- padding-right: 10px;
-}
-
-.code-search-box .note {
- margin-top: 4px;
- margin-left: 25px;
- opacity: 0;
- transition: opacity 0.3s ease;
-}
-
-.code-search-box input.touched ~ .note {
- opacity: 1;
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 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 without from 'lodash/without';
+import sortBy from 'lodash/sortBy';
+
+import {
+ addComponent,
+ getComponent as getComponentFromBucket,
+ addComponentChildren,
+ getComponentChildren,
+ addComponentBreadcrumbs,
+ getComponentBreadcrumbs
+} from './bucket';
+import { getChildren, getComponent, getBreadcrumbs } from '../../api/components';
+import { translate } from '../../helpers/l10n';
+
+const METRICS = [
+ 'ncloc',
+ 'code_smells',
+ 'bugs',
+ 'vulnerabilities',
+ 'duplicated_lines_density',
+ 'alert_status'
+];
+
+const METRICS_WITH_COVERAGE = [
+ ...METRICS,
+ 'coverage',
+ 'it_coverage',
+ 'overall_coverage'
+];
+
+const PAGE_SIZE = 100;
+
+function expandRootDir ({ components, total, ...other }) {
+ const rootDir = components.find(component => component.qualifier === 'DIR' && component.name === '/');
+ if (rootDir) {
+ return getChildren(rootDir.key, METRICS_WITH_COVERAGE).then(r => {
+ const nextComponents = without([...r.components, ...components], rootDir);
+ const nextTotal = total + r.components.length - /* root dir */ 1;
+ return { components: nextComponents, total: nextTotal, ...other };
+ });
+ } else {
+ return { components, total, ...other };
+ }
+}
+
+function prepareChildren (r) {
+ return {
+ components: r.components,
+ total: r.paging.total,
+ page: r.paging.pageIndex
+ };
+}
+
+function skipRootDir (breadcrumbs) {
+ return breadcrumbs.filter(component => {
+ return !(component.qualifier === 'DIR' && component.name === '/');
+ });
+}
+
+function storeChildrenBase (children) {
+ children.forEach(addComponent);
+}
+
+function storeChildrenBreadcrumbs (parentComponentKey, children) {
+ const parentBreadcrumbs = getComponentBreadcrumbs(parentComponentKey);
+ if (parentBreadcrumbs) {
+ children.forEach(child => {
+ const breadcrumbs = [...parentBreadcrumbs, child];
+ addComponentBreadcrumbs(child.key, breadcrumbs);
+ });
+ }
+}
+
+export function retrieveComponentBase (componentKey) {
+ const existing = getComponentFromBucket(componentKey);
+ if (existing) {
+ return Promise.resolve(existing);
+ }
+
+ return getComponent(componentKey, METRICS_WITH_COVERAGE).then(component => {
+ addComponent(component);
+ return component;
+ });
+}
+
+function retrieveComponentChildren (componentKey) {
+ const existing = getComponentChildren(componentKey);
+ if (existing) {
+ return Promise.resolve({
+ components: existing.children,
+ total: existing.total
+ });
+ }
+
+ return getChildren(componentKey, METRICS_WITH_COVERAGE, { ps: PAGE_SIZE, s: 'name' })
+ .then(prepareChildren)
+ .then(expandRootDir)
+ .then(r => {
+ addComponentChildren(componentKey, r.components, r.total);
+ storeChildrenBase(r.components);
+ storeChildrenBreadcrumbs(componentKey, r.components);
+ return r;
+ });
+}
+
+function retrieveComponentBreadcrumbs (componentKey) {
+ const existing = getComponentBreadcrumbs(componentKey);
+ if (existing) {
+ return Promise.resolve(existing);
+ }
+
+ return getBreadcrumbs({ key: componentKey })
+ .then(skipRootDir)
+ .then(breadcrumbs => {
+ addComponentBreadcrumbs(componentKey, breadcrumbs);
+ return breadcrumbs;
+ });
+}
+
+export function retrieveComponent (componentKey) {
+ return Promise.all([
+ retrieveComponentBase(componentKey),
+ retrieveComponentChildren(componentKey),
+ retrieveComponentBreadcrumbs(componentKey)
+ ]).then(r => {
+ return {
+ component: r[0],
+ components: r[1].components,
+ total: r[1].total,
+ page: r[1].page,
+ breadcrumbs: r[2]
+ };
+ });
+}
+
+export function loadMoreChildren (componentKey, page) {
+ return getChildren(componentKey, METRICS_WITH_COVERAGE, { ps: PAGE_SIZE, p: page })
+ .then(prepareChildren)
+ .then(expandRootDir)
+ .then(r => {
+ addComponentChildren(componentKey, r.components, r.total);
+ storeChildrenBase(r.components);
+ storeChildrenBreadcrumbs(componentKey, r.components);
+ return r;
+ });
+}
+
+export function parseError (error) {
+ try {
+ return error.response.json().then(r => {
+ return r.errors.map(error => error.msg).join('. ');
+ });
+ } catch (ex) {
+ return Promise.resolve(translate('default_error_message'));
+ }
+}
import ComponentsList from './ComponentsList';
import ListHeader from './ListHeader';
import Spinner from '../../components/Spinner';
-import SourceViewer from '../../../code/components/SourceViewer';
+import SourceViewer from '../../../../components/source-viewer/SourceViewer';
import ListFooter from '../../../../components/shared/list-footer';
export default class ListView extends React.Component {
import ComponentsList from './ComponentsList';
import ListHeader from './ListHeader';
import Spinner from '../../components/Spinner';
-import SourceViewer from '../../../code/components/SourceViewer';
+import SourceViewer from '../../../../components/source-viewer/SourceViewer';
import ListFooter from '../../../../components/shared/list-footer';
export default class TreeView extends React.Component {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 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 React from 'react';
+
+import BaseSourceViewer from './main';
+import { getPeriodDate, getPeriodLabel } from '../../helpers/periods';
+
+export default class SourceViewer extends React.Component {
+ static propTypes = {
+ component: React.PropTypes.shape({
+ id: React.PropTypes.string.isRequired
+ }).isRequired,
+ period: React.PropTypes.object
+ };
+
+ componentDidMount () {
+ this.renderSourceViewer();
+ }
+
+ shouldComponentUpdate (nextProps) {
+ return nextProps.component.id !== this.props.component.id;
+ }
+
+ componentWillUpdate () {
+ this.destroySourceViewer();
+ }
+
+ componentDidUpdate () {
+ this.renderSourceViewer();
+ }
+
+ componentWillUnmount () {
+ this.destroySourceViewer();
+ }
+
+ renderSourceViewer () {
+ this.sourceViewer = new BaseSourceViewer();
+ this.sourceViewer.render().$el.appendTo(this.refs.container);
+ this.sourceViewer.open(this.props.component.id);
+ this.sourceViewer.on('loaded', this.handleLoad.bind(this));
+ }
+
+ destroySourceViewer () {
+ this.sourceViewer.destroy();
+ }
+
+ handleLoad () {
+ const { period } = this.props;
+
+ if (period) {
+ const periodDate = getPeriodDate(period);
+ const periodLabel = getPeriodLabel(period);
+ this.sourceViewer.filterLinesByDate(periodDate, periodLabel);
+ }
+ }
+
+ render () {
+ return <div ref="container"/>;
+ }
+}
return metricKey.indexOf('new_') === 0;
}
+/**
+ * Check all types of coverage and return most suitable one
+ * @param {Array} measures
+ * @returns {string}
+ */
+export function selectCoverageMetric (measures) {
+ const hasOverallCoverage = !!measures.find(measure => measure.metric === 'overall_coverage');
+ const hasUTCoverage = !!measures.find(measure => measure.metric === 'coverage');
+ const hasITCoverage = !!measures.find(measure => measure.metric === 'it_coverage');
+
+ if (hasOverallCoverage && hasUTCoverage && hasITCoverage) {
+ return 'overall_';
+ } else if (hasITCoverage) {
+ return 'it_';
+ } else {
+ return '';
+ }
+}
+
/*
* Helpers
*/
},
renderCodeLink() {
- if (this.isView() || this.isDeveloper()) {
- return null;
- }
-
- const url = `/code/index?id=${encodeURIComponent(this.props.component.key)}`;
- return this.renderLink(url, translate('code.page'), '/code');
- },
-
- renderProjectsLink() {
- if (!this.isView()) {
+ if (this.isDeveloper()) {
return null;
}
- const url = `/view_projects/index?id=${encodeURIComponent(this.props.component.key)}`;
- return this.renderLink(url, translate('view_projects.page'), '/view_projects');
+ const url = `/code/?id=${encodeURIComponent(this.props.component.key)}`;
+ const header = this.isView() ? translate('view_projects.page') : translate('code.page');
+ return this.renderLink(url, header, '/code');
},
renderComponentIssuesLink() {
{this.renderComponentIssuesLink()}
{this.renderComponentMeasuresLink()}
{this.renderCodeLink()}
- {this.renderProjectsLink()}
{this.renderTools()}
{this.renderAdministration()}
</ul>
{this.renderComponentIssuesLink()}
{this.renderComponentMeasuresLink()}
{this.renderCodeLink()}
- {this.renderProjectsLink()}
{this.renderCustomDashboards()}
{this.renderTools()}
{this.renderAdministration()}
+++ /dev/null
-import chai, { expect } from 'chai';
-import sinon from 'sinon';
-import sinonChai from 'sinon-chai';
-import { shallow } from 'enzyme';
-import React from 'react';
-import TestUtils from 'react-addons-test-utils';
-
-import Breadcrumb from '../../../src/main/js/apps/code/components/Breadcrumb';
-import Breadcrumbs from '../../../src/main/js/apps/code/components/Breadcrumbs';
-import ComponentDetach from '../../../src/main/js/apps/code/components/ComponentDetach';
-import ComponentMeasure from '../../../src/main/js/apps/code/components/ComponentMeasure';
-import ComponentName from '../../../src/main/js/apps/code/components/ComponentName';
-import ComponentsEmpty from '../../../src/main/js/apps/code/components/ComponentsEmpty';
-import Truncated from '../../../src/main/js/apps/code/components/Truncated';
-
-import { getComponentUrl } from '../../../src/main/js/helpers/urls';
-import QualifierIcon from '../../../src/main/js/components/shared/qualifier-icon';
-
-
-chai.use(sinonChai);
-
-
-const measures = [
- { metric: 'ncloc', value: 9757 }
-];
-const exampleComponent = {
- uuid: 'A1',
- key: 'A',
- name: 'AA',
- qualifier: 'TRK',
- measures: measures
-};
-const exampleComponent2 = { uuid: 'B2', key: 'B' };
-const exampleComponent3 = { uuid: 'C3', key: 'C' };
-const exampleOnBrowse = sinon.spy();
-
-
-describe('Code :: Components', () => {
-
- describe('<Breadcrumb/>', () => {
- it('should render <ComponentName/>', () => {
- const output = shallow(
- <Breadcrumb
- component={exampleComponent}
- onBrowse={exampleOnBrowse}/>
- );
-
- expect(output.type())
- .to.equal(ComponentName);
- expect(output.props())
- .to.deep.equal({ component: exampleComponent, onBrowse: exampleOnBrowse })
- });
- });
-
- describe('<Breadcrumbs/>', () => {
- let output;
- let list;
-
- before(() => {
- output = shallow(
- <Breadcrumbs
- breadcrumbs={[exampleComponent, exampleComponent2, exampleComponent3]}
- onBrowse={exampleOnBrowse}/>);
- list = output.find(Breadcrumb);
- });
-
- it('should render list of <Breadcrumb/>s', () => {
- expect(list)
- .to.have.length(3);
- expect(list.at(0).prop('component'))
- .to.equal(exampleComponent);
- expect(list.at(1).prop('component'))
- .to.equal(exampleComponent2);
- expect(list.at(2).prop('component'))
- .to.equal(exampleComponent3);
- });
-
- it('should pass onBrowse to all components except the last one', () => {
- expect(list.at(0).prop('onBrowse'))
- .to.equal(exampleOnBrowse);
- expect(list.at(1).prop('onBrowse'))
- .to.equal(exampleOnBrowse);
- expect(list.at(2).prop('onBrowse'))
- .to.equal(null);
- });
- });
-
- describe('<ComponentDetach/>', () => {
- it('should render link', () => {
- const output = shallow(
- <ComponentDetach component={exampleComponent}/>);
- const expectedUrl = getComponentUrl(exampleComponent.key);
-
- expect(output.type())
- .to.equal('a');
- expect(output.prop('href'))
- .to.equal(expectedUrl);
- });
- });
-
- describe('<ComponentMeasure/>', () => {
- it('should render formatted measure', () => {
- const output = shallow(
- <ComponentMeasure
- component={exampleComponent}
- metricKey="ncloc"
- metricType="SHORT_INT"/>);
-
- expect(output.text())
- .to.equal('9.8k');
- });
-
- it('should not render measure', () => {
- const output = shallow(
- <ComponentMeasure
- component={exampleComponent}
- metricKey="random"
- metricType="SHORT_INT"/>);
-
- expect(output.text())
- .to.equal('');
- });
- });
-
- describe('<ComponentName/>', () => {
- it('should render <QualifierIcon/>', () => {
- const output = shallow(
- <ComponentName
- component={exampleComponent}
- onBrowse={exampleOnBrowse}/>);
- const findings = output.find(QualifierIcon);
-
- expect(findings)
- .to.have.length(1);
- expect(findings.first().prop('qualifier'))
- .to.equal('TRK');
- });
-
- it('should render link to component', () => {
- const output = shallow(
- <ComponentName
- component={exampleComponent}
- onBrowse={exampleOnBrowse}/>);
- const findings = output.find('a');
-
- expect(findings)
- .to.have.length(1);
- expect(findings.first().text())
- .to.equal('AA');
- });
-
- it('should not render link to component', () => {
- const output = shallow(
- <ComponentName
- component={exampleComponent}
- onBrowse={null}/>);
- const findings = output.find('span');
-
- expect(output.find('a'))
- .to.have.length(0);
- expect(findings)
- .to.have.length(1);
- expect(findings.first().text())
- .to.equal('AA');
- });
-
- it('should browse on click', () => {
- const spy = sinon.spy();
- const preventDefaultSpy = sinon.spy();
- const output = shallow(
- <ComponentName
- component={exampleComponent}
- onBrowse={spy}/>);
- const findings = output.find('a');
-
- findings.first().simulate('click', { preventDefault: preventDefaultSpy });
-
- expect(preventDefaultSpy).to.have.been.called;
- expect(spy).to.have.been.calledWith(exampleComponent);
- });
- });
-
- describe('<ComponentsEmpty/>', () => {
- it('should render', () => {
- const output = shallow(<ComponentsEmpty/>);
-
- expect(output.text())
- .to.include('no_results');
- });
- });
-
- describe('<Truncated/>', () => {
- it('should render and set title', () => {
- const output = shallow(<Truncated title="ABC">123</Truncated>);
-
- expect(output.type())
- .to.equal('span');
- expect(output.text())
- .to.equal('123');
- expect(output.prop('title'))
- .to.equal('ABC');
- });
- });
-});
+++ /dev/null
-import { expect } from 'chai';
-
-import { current, bucket, initialState } from '../../../src/main/js/apps/code/reducers';
-import {
- initComponentAction,
- browseAction,
- searchAction,
- updateQueryAction,
- selectNext,
- selectPrev,
- startFetching,
- stopFetching,
- raiseError
-} from '../../../src/main/js/apps/code/actions';
-
-
-const exampleComponent = { key: 'A' };
-const exampleComponent2 = { key: 'B' };
-const exampleComponents = [
- { key: 'B' },
- { key: 'C' }
-];
-
-
-describe('Code :: Store', () => {
- //describe('action creators');
-
- describe('reducers', () => {
- describe('current', () => {
- describe('fetching', () => {
- it('should be set to true', () => {
- expect(current({ ...initialState, fetching: false }, startFetching()).fetching)
- .to.equal(true);
- });
-
- it('should be false', () => {
- expect(current({ ...initialState, fetching: true }, stopFetching()).fetching)
- .to.equal(false);
- });
- });
- describe('baseComponent', () => {
- it('should be set', () => {
- const component = {};
- expect(current(initialState, browseAction(component)).baseComponent)
- .to.equal(component);
- });
-
- it('should not be set for components with source code', () => {
- const file = { qualifier: 'FIL' };
- expect(current(initialState, browseAction(file, exampleComponents)).baseComponent)
- .to.be.null;
- const test = { qualifier: 'UTS' };
- expect(current(initialState, browseAction(test, exampleComponents)).baseComponent)
- .to.be.null;
- });
- });
- describe('components', () => {
- it('should be set', () => {
- const component = {};
- expect(current(initialState, browseAction(component, exampleComponents)).components)
- .to.deep.equal(exampleComponents);
- });
-
- it('should sort components by name', () => {
- const component = {};
- const componentsBefore = [
- { key: 'A', name: 'B' },
- { key: 'B', name: 'A' }
- ];
- const componentsAfter = [
- { key: 'B', name: 'A' },
- { key: 'A', name: 'B' }
- ];
- expect(current(initialState, browseAction(component, componentsBefore)).components)
- .to.deep.equal(componentsAfter);
- });
-
- it('should sort components by qualifier and then by name', () => {
- const component = {};
- const componentsBefore = [
- { key: 'A', name: 'A', qualifier: 'DIR' },
- { key: 'B', name: 'B', qualifier: 'FIL' }
- ];
- const componentsAfter = [
- { key: 'B', name: 'B', qualifier: 'FIL' },
- { key: 'A', name: 'A', qualifier: 'DIR' }
- ];
- expect(current(initialState, browseAction(component, componentsBefore)).components)
- .to.deep.equal(componentsAfter);
- });
-
- it('should not be set for components with source code', () => {
- const file = { qualifier: 'FIL' };
- expect(current(initialState, browseAction(file, exampleComponents)).components)
- .to.be.null;
- const test = { qualifier: 'UTS' };
- expect(current(initialState, browseAction(test, exampleComponents)).components)
- .to.be.null;
- });
- });
- describe('breadcrumbs', () => {
- it('should be set', () => {
- expect(current(initialState, browseAction(exampleComponent, [], exampleComponents)).breadcrumbs)
- .to.deep.equal(exampleComponents);
- });
-
- it('should respect baseBreadcrumbs', () => {
- const baseBreadcrumbs = [{ key: 'BASE1' }];
- const breadcrumbsBefore = [{ key: 'BASE1' }, { key: 'BASE2' }, { key: 'C' }];
- const breadcrumbsAfter = [{ key: 'BASE2' }, { key: 'C' }];
- expect(current(
- { ...initialState, baseBreadcrumbs },
- browseAction(exampleComponent, [], breadcrumbsBefore)).breadcrumbs
- ).to.deep.equal(breadcrumbsAfter);
- });
- });
- describe('sourceViewer', () => {
- it('should be set for components with source code', () => {
- const file = { qualifier: 'FIL' };
- expect(current(initialState, browseAction(file, exampleComponents)).sourceViewer)
- .to.equal(file);
- const test = { qualifier: 'UTS' };
- expect(current(initialState, browseAction(test, exampleComponents)).sourceViewer)
- .to.equal(test);
- });
-
- it('should not be set for components without source code', () => {
- const project = { qualifier: 'TRK' };
- expect(current(initialState, browseAction(project, exampleComponents)).sourceViewer)
- .to.be.null;
- const unknown = {};
- expect(current(initialState, browseAction(unknown, exampleComponents)).sourceViewer)
- .to.be.null;
- });
-
- it('should be reset', () => {
- const stateBefore = Object.assign({}, initialState, { sourceViewer: exampleComponent });
- expect(current(stateBefore, searchAction(exampleComponents)).sourceViewer)
- .to.be.null;
- });
- });
- describe('coverageMetric', () => {
- it('should be set to "coverage"', () => {
- const componentWithCoverage = {
- ...exampleComponent,
- measures: [
- { metric: 'coverage', value: 13 }
- ]
- };
-
- expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
- .to.equal('coverage');
- });
-
- it('should be set to "it_coverage"', () => {
- const componentWithCoverage = {
- ...exampleComponent,
- measures: [
- { metric: 'it_coverage', value: 13 }
- ]
- };
-
- expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
- .to.equal('it_coverage');
- });
-
- it('should be set to "overall_coverage"', () => {
- const componentWithCoverage = {
- ...exampleComponent,
- measures: [
- { metric: 'coverage', value: 11 },
- { metric: 'it_coverage', value: 12 },
- { metric: 'overall_coverage', value: 13 }
- ]
- };
-
- expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
- .to.equal('overall_coverage');
- });
-
- it('should fallback to "it_coverage"', () => {
- const componentWithCoverage = {
- ...exampleComponent,
- measures: []
- };
-
- expect(current(initialState, initComponentAction(componentWithCoverage)).coverageMetric)
- .to.equal('it_coverage');
- });
- });
- describe('baseBreadcrumbs', () => {
- it('should be empty', () => {
- const component = { key: 'A' };
- const breadcrumbs = [{ key: 'A' }];
-
- expect(current(initialState, initComponentAction(component, breadcrumbs)).baseBreadcrumbs)
- .to.have.length(0);
- });
-
- it('should set baseBreadcrumbs', () => {
- const component = { key: 'A' };
- const breadcrumbs = [{ key: 'BASE' }, { key: 'A' }];
-
- expect(current(initialState, initComponentAction(component, breadcrumbs)).baseBreadcrumbs)
- .to.have.length(1);
- });
- });
- describe('searchResults', () => {
- it('should be set', () => {
- const results = [{ key: 'A' }, { key: 'B' }];
- expect(current(initialState, searchAction(results)).searchResults)
- .to.deep.equal(results)
- });
-
- it('should be reset', () => {
- const results = [{ key: 'A' }, { key: 'B' }];
- const stateBefore = Object.assign({}, initialState, { searchResults: results });
- expect(current(stateBefore, browseAction(exampleComponent)).searchResults)
- .to.be.null;
- });
- });
- describe('searchQuery', () => {
- it('should be set', () => {
- expect(current(initialState, updateQueryAction('query')).searchQuery)
- .to.equal('query');
- });
-
- it('should be reset', () => {
- const stateBefore = Object.assign({}, initialState, { searchQuery: 'query' });
- expect(current(stateBefore, browseAction(exampleComponent)).searchQuery)
- .to.equal('');
- });
- });
- describe('searchSelectedItem', () => {
- it('should be set to the first result', () => {
- const results = [exampleComponent, exampleComponent2];
- expect(current(initialState, searchAction(results)).searchSelectedItem)
- .to.equal(exampleComponent);
- });
-
- it('should select next', () => {
- const results = [exampleComponent, exampleComponent2];
- const stateBefore = current(initialState, searchAction(results));
- const stateAfter = current(stateBefore, selectNext());
- expect(stateAfter.searchSelectedItem)
- .to.equal(exampleComponent2);
- });
-
- it('should not select next', () => {
- const results = [exampleComponent, exampleComponent2];
- const stateBefore = Object.assign({}, current(initialState, searchAction(results)), {
- searchSelectedItem: exampleComponent2
- });
- expect(current(stateBefore, selectNext()).searchSelectedItem)
- .to.equal(exampleComponent2);
- });
-
- it('should select prev', () => {
- const results = [exampleComponent, exampleComponent2];
- const stateBefore = Object.assign({}, current(initialState, searchAction(results)), {
- searchSelectedItem: exampleComponent2
- });
- expect(current(stateBefore, selectPrev()).searchSelectedItem)
- .to.equal(exampleComponent);
- });
-
- it('should not select prev', () => {
- const results = [exampleComponent, exampleComponent2];
- const stateBefore = current(initialState, searchAction(results));
- expect(current(stateBefore, selectPrev()).searchSelectedItem)
- .to.equal(exampleComponent);
- });
-
- it('should ignore if no results', () => {
- expect(current(initialState, selectNext()).searchSelectedItem)
- .to.be.null;
- expect(current(initialState, selectPrev()).searchSelectedItem)
- .to.be.null;
- });
-
- it('should be reset on browse', () => {
- const results = [exampleComponent, exampleComponent2];
- const stateBefore = current(initialState, searchAction(results));
- const stateAfter = current(stateBefore, browseAction(exampleComponent));
- expect(stateAfter.searchSelectedItem)
- .to.be.null;
- });
- });
- describe('errorMessage', () => {
- it('should be set', () => {
- expect(current(initialState, raiseError('error!')).errorMessage)
- .to.equal('error!');
- });
-
- it('should be reset', () => {
- const stateBefore = Object.assign({}, initialState, { errorMessage: 'error!' });
- expect(current(stateBefore, browseAction(exampleComponent)).errorMessage)
- .to.be.null;
- expect(current(stateBefore, searchAction(exampleComponents)).errorMessage)
- .to.be.null;
- });
- });
- });
- describe('bucket', () => {
- it('should add initial component', () => {
- expect(bucket([], initComponentAction(exampleComponent)))
- .to.deep.equal([exampleComponent]);
- });
-
- it('should add browsed component', () => {
- const componentBefore = { key: 'A' };
- const childrenBefore = [{ key: 'B' }];
- const breadcrumbsBefore = [{ key: 'A' }];
-
- const bucketAfter = [
- { key: 'A', breadcrumbs: [{ key: 'A' }], children: [{ key: 'B' }], total: 0 },
- { key: 'B', breadcrumbs: [{ key: 'A' }, { key: 'B' }] }
- ];
-
- expect(bucket([], browseAction(componentBefore, childrenBefore, breadcrumbsBefore)))
- .to.deep.equal(bucketAfter);
- });
-
- it('should merge new components', () => {
- const componentBefore = { key: 'A' };
- const childrenBefore = [{ key: 'B' }];
- const breadcrumbsBefore = [{ key: 'A' }];
-
- const bucketBefore = [
- { key: 'A' },
- { key: 'B' }
- ];
-
- const bucketAfter = [
- {
- key: 'A',
- breadcrumbs: [{ key: 'A' }],
- children: [{ key: 'B' }],
- total: 0
- },
- {
- key: 'B',
- breadcrumbs: [{ key: 'A' }, { key: 'B' }]
- }
- ];
-
- expect(bucket(bucketBefore, browseAction(componentBefore, childrenBefore, breadcrumbsBefore)))
- .to.deep.equal(bucketAfter);
- });
-
- it('should work twice in a row', () => {
- const componentA = { key: 'A' };
- const childrenA = [{ key: 'B' }];
- const breadcrumbsA = [{ key: 'A' }];
-
- const componentB = { key: 'B' };
- const childrenB = [{ key: 'C' }];
- const breadcrumbsB = [{ key: 'A' }, { key: 'B' }];
-
- const bucketAfter = [
- {
- key: 'A',
- breadcrumbs: [{ key: 'A' }],
- children: [{ key: 'B' }],
- total: 0
- },
- {
- key: 'B',
- breadcrumbs: [{ key: 'A' }, { key: 'B' }],
- children: [{ key: 'C' }],
- total: 0
- },
- {
- key: 'C',
- breadcrumbs: [{ key: 'A' }, { key: 'B' }, { key: 'C' }]
- }
- ];
-
- const afterFirstPass = bucket([], browseAction(componentA, childrenA, breadcrumbsA));
- const afterSecondPass = bucket(afterFirstPass, browseAction(componentB, childrenB, breadcrumbsB));
-
- expect(afterSecondPass)
- .to.deep.equal(bucketAfter);
- });
- });
- });
-});