From 5bf0d2cb3e25a4c629e49110b6bba0fb8c40959d Mon Sep 17 00:00:00 2001 From: Pascal Mugnier Date: Mon, 4 Jun 2018 11:14:17 +0200 Subject: [PATCH] SONAR-10709 Web API page looses filtering after clicking on endpoint --- .../js/apps/web-api/components/Domain.tsx | 18 ++-- .../main/js/apps/web-api/components/Menu.tsx | 14 +-- .../js/apps/web-api/components/Search.tsx | 16 ++-- .../js/apps/web-api/components/WebApiApp.tsx | 94 +++++++++---------- .../components/__tests__/Domain-test.tsx | 21 ++--- .../components/__tests__/Menu-test.tsx | 21 +++-- .../components/__tests__/Search-test.tsx | 3 +- .../__snapshots__/Menu-test.tsx.snap | 77 +++++++++++++-- .../__snapshots__/Search-test.tsx.snap | 1 + .../src/main/js/apps/web-api/utils.ts | 42 +++++++-- 10 files changed, 190 insertions(+), 117 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx index 4c9bbcc0426..bf7798134fd 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx @@ -21,20 +21,16 @@ import * as React from 'react'; import Action from './Action'; import DeprecatedBadge from './DeprecatedBadge'; import InternalBadge from './InternalBadge'; -import { getActionKey, actionsFilter } from '../utils'; +import { getActionKey, actionsFilter, Query } from '../utils'; import { Domain as DomainType } from '../../../api/web-api'; interface Props { domain: DomainType; - showDeprecated: boolean; - showInternal: boolean; - searchQuery: string; + query: Query; } -export default function Domain({ domain, showInternal, showDeprecated, searchQuery }: Props) { - const filteredActions = domain.actions.filter(action => - actionsFilter(showDeprecated, showInternal, searchQuery, domain, action) - ); +export default function Domain({ domain, query }: Props) { + const filteredActions = domain.actions.filter(action => actionsFilter(query, domain, action)); return (
@@ -64,11 +60,11 @@ export default function Domain({ domain, showInternal, showDeprecated, searchQue
{filteredActions.map(action => ( ))}
diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx index 334979d242c..b45675c25e9 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx @@ -22,24 +22,20 @@ import { Link } from 'react-router'; import * as classNames from 'classnames'; import DeprecatedBadge from './DeprecatedBadge'; import InternalBadge from './InternalBadge'; -import { isDomainPathActive, actionsFilter } from '../utils'; +import { isDomainPathActive, actionsFilter, Query, serializeQuery } from '../utils'; import { Domain } from '../../../api/web-api'; interface Props { domains: Domain[]; - showDeprecated: boolean; - showInternal: boolean; - searchQuery: string; + query: Query; splat: string; } export default function Menu(props: Props) { - const { domains, showInternal, showDeprecated, searchQuery, splat } = props; + const { domains, query, splat } = props; const filteredDomains = (domains || []) .map(domain => { - const filteredActions = domain.actions.filter(action => - actionsFilter(showDeprecated, showInternal, searchQuery, domain, action) - ); + const filteredActions = domain.actions.filter(action => actionsFilter(query, domain, action)); return { ...domain, filteredActions }; }) .filter(domain => domain.filteredActions.length); @@ -53,7 +49,7 @@ export default function Menu(props: Props) { active: isDomainPathActive(domain.path, splat) })} key={domain.path} - to={'/web_api/' + domain.path}> + to={{ pathname: '/web_api/' + domain.path, query: serializeQuery(query) }}>

{domain.path} {domain.deprecated && } diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Search.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Search.tsx index 49bc4e20220..09a1f319863 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Search.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Search.tsx @@ -22,26 +22,30 @@ import Checkbox from '../../../components/controls/Checkbox'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import { translate } from '../../../helpers/l10n'; import SearchBox from '../../../components/controls/SearchBox'; +import { Query } from '../utils'; interface Props { - showDeprecated: boolean; - showInternal: boolean; + query: Query; onSearch: (search: string) => void; onToggleInternal: () => void; onToggleDeprecated: () => void; } export default function Search(props: Props) { - const { showInternal, showDeprecated, onToggleInternal, onToggleDeprecated } = props; + const { query, onToggleInternal, onToggleDeprecated } = props; return (
- +
- + {translate('api_documentation.show_internal')}
- + {translate('api_documentation.show_deprecated')} diff --git a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx index 0b79a15c483..77630d3f8a4 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/WebApiApp.tsx @@ -26,21 +26,20 @@ import Search from './Search'; import Domain from './Domain'; import { Domain as DomainType, fetchWebApi } from '../../../api/web-api'; import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; -import { getActionKey, isDomainPathActive } from '../utils'; +import { getActionKey, isDomainPathActive, Query, serializeQuery, parseQuery } from '../utils'; import { scrollToElement } from '../../../helpers/scrolling'; import { translate } from '../../../helpers/l10n'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; +import { RawQuery } from '../../../helpers/query'; import '../styles/web-api.css'; interface Props { + location: { pathname: string; query: RawQuery }; params: { splat?: string }; } interface State { domains: DomainType[]; - searchQuery: string; - showDeprecated: boolean; - showInternal: boolean; } export default class WebApiApp extends React.PureComponent { @@ -52,12 +51,7 @@ export default class WebApiApp extends React.PureComponent { constructor(props: Props) { super(props); - this.state = { - domains: [], - searchQuery: '', - showDeprecated: false, - showInternal: false - }; + this.state = { domains: [] }; } componentDidMount() { @@ -103,47 +97,62 @@ export default class WebApiApp extends React.PureComponent { } }; + updateQuery = (newQuery: Partial) => { + const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery }); + this.context.router.push({ pathname: this.props.location.pathname, query }); + }; + toggleInternalInitially() { const splat = this.props.params.splat || ''; - const { domains, showInternal } = this.state; - - if (!showInternal) { - domains.forEach(domain => { - if (domain.path === splat && domain.internal) { - this.setState({ showInternal: true }); + const { domains } = this.state; + const query = parseQuery(this.props.location.query); + + if (!query.internal && splat) { + const domain = domains.find(domain => domain.path.startsWith(splat)); + if (domain) { + let action; + if (domain.path !== splat) { + action = domain.actions.find(action => getActionKey(domain.path, action.key) === splat); } - domain.actions.forEach(action => { - const actionKey = getActionKey(domain.path, action.key); - if (actionKey === splat && action.internal) { - this.setState({ showInternal: true }); - } - }); - }); + if (domain.internal || (action && action.internal)) { + this.updateQuery({ internal: true }); + } + } } } - handleSearch = (searchQuery: string) => this.setState({ searchQuery }); + handleSearch = (search: string) => { + this.updateQuery({ search }); + }; handleToggleInternal = () => { const splat = this.props.params.splat || ''; const { router } = this.context; const { domains } = this.state; const domain = domains.find(domain => isDomainPathActive(domain.path, splat)); - const showInternal = !this.state.showInternal; + const query = parseQuery(this.props.location.query); + const internal = !query.internal; - if (domain && domain.internal && !showInternal) { - router.push('/web_api'); + if (domain && domain.internal && !internal) { + router.push({ + pathname: '/web_api', + query: { ...serializeQuery(query), internal: false } + }); + return; } - this.setState({ showInternal }); + this.updateQuery({ internal }); }; - handleToggleDeprecated = () => - this.setState(state => ({ showDeprecated: !state.showDeprecated })); + handleToggleDeprecated = () => { + const query = parseQuery(this.props.location.query); + this.updateQuery({ deprecated: !query.deprecated }); + }; render() { const splat = this.props.params.splat || ''; - const { domains, showInternal, showDeprecated, searchQuery } = this.state; + const query = parseQuery(this.props.location.query); + const { domains } = this.state; const domain = domains.find(domain => isDomainPathActive(domain.path, splat)); @@ -163,20 +172,13 @@ export default class WebApiApp extends React.PureComponent {
- +

@@ -185,15 +187,7 @@ export default class WebApiApp extends React.PureComponent {
- {domain && ( - - )} + {domain && }
diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Domain-test.tsx b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Domain-test.tsx index ddc3c2921d0..cdf43ee79b6 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Domain-test.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Domain-test.tsx @@ -38,16 +38,17 @@ const DOMAIN = { }; const DEFAULT_PROPS = { domain: DOMAIN, - showDeprecated: false, - showInternal: false, - searchQuery: '' + query: { search: '', deprecated: false, internal: false } }; +const SHOW_DEPRECATED = { search: '', deprecated: true, internal: false }; +const SHOW_INTERNAL = { search: '', deprecated: false, internal: true }; +const SEARCH_FOO = { search: 'Foo', deprecated: false, internal: false }; it('should render deprecated actions', () => { const action = { ...ACTION, deprecatedSince: '5.0' }; const domain = { ...DOMAIN, actions: [action] }; expect( - shallow() + shallow() ).toMatchSnapshot(); }); @@ -55,7 +56,7 @@ it('should not render deprecated actions', () => { const action = { ...ACTION, deprecatedSince: '5.0' }; const domain = { ...DOMAIN, actions: [action] }; expect( - shallow() + shallow() ).toMatchSnapshot(); }); @@ -63,23 +64,21 @@ it('should render internal actions', () => { const action = { ...ACTION, internal: true }; const domain = { ...DOMAIN, actions: [action] }; expect( - shallow() + shallow() ).toMatchSnapshot(); }); it('should not render internal actions', () => { const action = { ...ACTION, internal: true }; const domain = { ...DOMAIN, actions: [action] }; - expect( - shallow() - ).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); it('should render only actions matching the query', () => { const actions = [ACTION, { ...ACTION, key: 'bar', description: 'Bar desc' }]; const domain = { ...DOMAIN, actions }; expect( - shallow() + shallow() ).toMatchSnapshot(); }); @@ -91,6 +90,6 @@ it('should also render actions with a description matching the query', () => { ]; const domain = { ...DOMAIN, actions }; expect( - shallow() + shallow() ).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx index 89b885c05cc..5c2a39fb7bf 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Menu-test.tsx @@ -45,12 +45,15 @@ const DOMAIN2 = { }; const PROPS = { domains: [DOMAIN1, DOMAIN2], - showDeprecated: false, - showInternal: false, - searchQuery: '', + query: { search: '', deprecated: false, internal: false }, splat: '' }; +const SHOW_DEPRECATED = { search: '', deprecated: true, internal: false }; +const SHOW_INTERNAL = { search: '', deprecated: false, internal: true }; +const SEARCH_FOO = { search: 'Foo', deprecated: false, internal: false }; +const SEARCH_BAR = { search: 'Bar', deprecated: false, internal: false }; + it('should render deprecated domains', () => { const domain = { ...DOMAIN2, @@ -58,7 +61,7 @@ it('should render deprecated domains', () => { actions: [{ ...ACTION, deprecatedSince: '5.0' }] }; const domains = [DOMAIN1, domain]; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); it('should not render deprecated domains', () => { @@ -68,19 +71,19 @@ it('should not render deprecated domains', () => { actions: [{ ...ACTION, deprecatedSince: '5.0' }] }; const domains = [DOMAIN1, domain]; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); it('should render internal domains', () => { const domain = { ...DOMAIN2, internal: true, actions: [{ ...ACTION, internal: true }] }; const domains = [DOMAIN1, domain]; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); it('should not render internal domains', () => { const domain = { ...DOMAIN2, internal: true, actions: [{ ...ACTION, internal: true }] }; const domains = [DOMAIN1, domain]; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); it('should render only domains with an action matching the query', () => { @@ -89,7 +92,7 @@ it('should render only domains with an action matching the query', () => { actions: [{ ...ACTION, key: 'bar', path: 'bar', description: 'Bar Desc' }] }; const domains = [DOMAIN1, domain]; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); it('should also render domains with an actions description matching the query', () => { @@ -100,5 +103,5 @@ it('should also render domains with an actions description matching the query', actions: [{ ...ACTION, key: 'baz', path: 'baz', description: 'barbaz' }] }; const domains = [DOMAIN1, DOMAIN2, domain]; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Search-test.tsx index c4a67af96dd..f476ceccc49 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Search-test.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/Search-test.tsx @@ -22,8 +22,7 @@ import { shallow } from 'enzyme'; import Search from '../Search'; const PROPS = { - showDeprecated: false, - showInternal: false, + query: { search: '', deprecated: false, internal: false }, onSearch: () => {}, onToggleInternal: () => {}, onToggleDeprecated: () => {} diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap index 57099cb0383..b2479be343b 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Menu-test.tsx.snap @@ -12,7 +12,14 @@ exports[`should also render domains with an actions description matching the que key="bar" onlyActiveOnIndex={false} style={Object {}} - to="/web_api/bar" + to={ + Object { + "pathname": "/web_api/bar", + "query": Object { + "query": "Bar", + }, + } + } >

{ return true; }; + +export const parseQuery = memoize((urlQuery: RawQuery): Query => ({ + search: parseAsString(urlQuery['query']), + deprecated: parseAsOptionalBoolean(urlQuery['deprecated']) || false, + internal: parseAsOptionalBoolean(urlQuery['internal']) || false +})); + +export const serializeQuery = memoize((query: Query): RawQuery => + cleanQuery({ + query: query.search ? serializeString(query.search) : undefined, + deprecated: query.deprecated || undefined, + internal: query.internal || undefined + }) +); -- 2.39.5