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 (
<div className="web-api-domain">
<div className="web-api-domain-actions">
{filteredActions.map(action => (
<Action
- key={getActionKey(domain.path, action.key)}
action={action}
domain={domain}
- showDeprecated={showDeprecated}
- showInternal={showInternal}
+ key={getActionKey(domain.path, action.key)}
+ showDeprecated={query.deprecated}
+ showInternal={query.internal}
/>
))}
</div>
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);
active: isDomainPathActive(domain.path, splat)
})}
key={domain.path}
- to={'/web_api/' + domain.path}>
+ to={{ pathname: '/web_api/' + domain.path, query: serializeQuery(query) }}>
<h3 className="list-group-item-heading">
{domain.path}
{domain.deprecated && <DeprecatedBadge />}
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 (
<div className="web-api-search">
<div>
- <SearchBox onChange={props.onSearch} placeholder={translate('api_documentation.search')} />
+ <SearchBox
+ onChange={props.onSearch}
+ placeholder={translate('api_documentation.search')}
+ value={query.search}
+ />
</div>
<div className="big-spacer-top">
- <Checkbox checked={showInternal} className="text-middle" onCheck={onToggleInternal}>
+ <Checkbox checked={query.internal} className="text-middle" onCheck={onToggleInternal}>
<span className="little-spacer-left">{translate('api_documentation.show_internal')}</span>
</Checkbox>
<HelpTooltip
</div>
<div className="spacer-top">
- <Checkbox checked={showDeprecated} className="text-middle" onCheck={onToggleDeprecated}>
+ <Checkbox checked={query.deprecated} className="text-middle" onCheck={onToggleDeprecated}>
<span className="little-spacer-left">
{translate('api_documentation.show_deprecated')}
</span>
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<Props, State> {
constructor(props: Props) {
super(props);
- this.state = {
- domains: [],
- searchQuery: '',
- showDeprecated: false,
- showInternal: false
- };
+ this.state = { domains: [] };
}
componentDidMount() {
}
};
+ updateQuery = (newQuery: Partial<Query>) => {
+ 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));
</div>
<Search
- showDeprecated={showDeprecated}
- showInternal={showInternal}
onSearch={this.handleSearch}
- onToggleInternal={this.handleToggleInternal}
onToggleDeprecated={this.handleToggleDeprecated}
+ onToggleInternal={this.handleToggleInternal}
+ query={query}
/>
- <Menu
- domains={this.state.domains}
- showDeprecated={showDeprecated}
- showInternal={showInternal}
- searchQuery={searchQuery}
- splat={splat}
- />
+ <Menu domains={this.state.domains} query={query} splat={splat} />
</div>
</div>
</div>
<div className="layout-page-main">
<div className="layout-page-main-inner">
- {domain && (
- <Domain
- key={domain.path}
- domain={domain}
- showDeprecated={showDeprecated}
- showInternal={showInternal}
- searchQuery={searchQuery}
- />
- )}
+ {domain && <Domain domain={domain} key={domain.path} query={query} />}
</div>
</div>
</div>
};
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(<Domain {...DEFAULT_PROPS} domain={domain} showDeprecated={true} />)
+ shallow(<Domain {...DEFAULT_PROPS} domain={domain} query={SHOW_DEPRECATED} />)
).toMatchSnapshot();
});
const action = { ...ACTION, deprecatedSince: '5.0' };
const domain = { ...DOMAIN, actions: [action] };
expect(
- shallow(<Domain {...DEFAULT_PROPS} domain={domain} showDeprecated={false} />)
+ shallow(<Domain {...DEFAULT_PROPS} domain={domain} query={SHOW_INTERNAL} />)
).toMatchSnapshot();
});
const action = { ...ACTION, internal: true };
const domain = { ...DOMAIN, actions: [action] };
expect(
- shallow(<Domain {...DEFAULT_PROPS} domain={domain} showInternal={true} />)
+ shallow(<Domain {...DEFAULT_PROPS} domain={domain} query={SHOW_INTERNAL} />)
).toMatchSnapshot();
});
it('should not render internal actions', () => {
const action = { ...ACTION, internal: true };
const domain = { ...DOMAIN, actions: [action] };
- expect(
- shallow(<Domain {...DEFAULT_PROPS} domain={domain} showInternal={false} />)
- ).toMatchSnapshot();
+ expect(shallow(<Domain {...DEFAULT_PROPS} domain={domain} />)).toMatchSnapshot();
});
it('should render only actions matching the query', () => {
const actions = [ACTION, { ...ACTION, key: 'bar', description: 'Bar desc' }];
const domain = { ...DOMAIN, actions };
expect(
- shallow(<Domain {...DEFAULT_PROPS} domain={domain} searchQuery="Foo" />)
+ shallow(<Domain {...DEFAULT_PROPS} domain={domain} query={SEARCH_FOO} />)
).toMatchSnapshot();
});
];
const domain = { ...DOMAIN, actions };
expect(
- shallow(<Domain {...DEFAULT_PROPS} domain={domain} searchQuery="Foo" />)
+ shallow(<Domain {...DEFAULT_PROPS} domain={domain} query={SEARCH_FOO} />)
).toMatchSnapshot();
});
};
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,
actions: [{ ...ACTION, deprecatedSince: '5.0' }]
};
const domains = [DOMAIN1, domain];
- expect(shallow(<Menu {...PROPS} domains={domains} showDeprecated={true} />)).toMatchSnapshot();
+ expect(shallow(<Menu {...PROPS} domains={domains} query={SHOW_DEPRECATED} />)).toMatchSnapshot();
});
it('should not render deprecated domains', () => {
actions: [{ ...ACTION, deprecatedSince: '5.0' }]
};
const domains = [DOMAIN1, domain];
- expect(shallow(<Menu {...PROPS} domains={domains} showDeprecated={false} />)).toMatchSnapshot();
+ expect(shallow(<Menu {...PROPS} domains={domains} />)).toMatchSnapshot();
});
it('should render internal domains', () => {
const domain = { ...DOMAIN2, internal: true, actions: [{ ...ACTION, internal: true }] };
const domains = [DOMAIN1, domain];
- expect(shallow(<Menu {...PROPS} domains={domains} showInternal={true} />)).toMatchSnapshot();
+ expect(shallow(<Menu {...PROPS} domains={domains} query={SHOW_INTERNAL} />)).toMatchSnapshot();
});
it('should not render internal domains', () => {
const domain = { ...DOMAIN2, internal: true, actions: [{ ...ACTION, internal: true }] };
const domains = [DOMAIN1, domain];
- expect(shallow(<Menu {...PROPS} domains={domains} showInternal={false} />)).toMatchSnapshot();
+ expect(shallow(<Menu {...PROPS} domains={domains} />)).toMatchSnapshot();
});
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(<Menu {...PROPS} domains={domains} searchQuery="Foo" />)).toMatchSnapshot();
+ expect(shallow(<Menu {...PROPS} domains={domains} query={SEARCH_FOO} />)).toMatchSnapshot();
});
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(<Menu {...PROPS} domains={domains} searchQuery="Bar" />)).toMatchSnapshot();
+ expect(shallow(<Menu {...PROPS} domains={domains} query={SEARCH_BAR} />)).toMatchSnapshot();
});
import Search from '../Search';
const PROPS = {
- showDeprecated: false,
- showInternal: false,
+ query: { search: '', deprecated: false, internal: false },
onSearch: () => {},
onToggleInternal: () => {},
onToggleDeprecated: () => {}
key="bar"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/bar"
+ to={
+ Object {
+ "pathname": "/web_api/bar",
+ "query": Object {
+ "query": "Bar",
+ },
+ }
+ }
>
<h3
className="list-group-item-heading"
key="baz"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/baz"
+ to={
+ Object {
+ "pathname": "/web_api/baz",
+ "query": Object {
+ "query": "Bar",
+ },
+ }
+ }
>
<h3
className="list-group-item-heading"
key="foo"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/foo"
+ to={
+ Object {
+ "pathname": "/web_api/foo",
+ "query": Object {},
+ }
+ }
>
<h3
className="list-group-item-heading"
key="foo"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/foo"
+ to={
+ Object {
+ "pathname": "/web_api/foo",
+ "query": Object {},
+ }
+ }
>
<h3
className="list-group-item-heading"
key="foo"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/foo"
+ to={
+ Object {
+ "pathname": "/web_api/foo",
+ "query": Object {
+ "deprecated": true,
+ },
+ }
+ }
>
<h3
className="list-group-item-heading"
key="bar"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/bar"
+ to={
+ Object {
+ "pathname": "/web_api/bar",
+ "query": Object {
+ "deprecated": true,
+ },
+ }
+ }
>
<h3
className="list-group-item-heading"
key="foo"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/foo"
+ to={
+ Object {
+ "pathname": "/web_api/foo",
+ "query": Object {
+ "internal": true,
+ },
+ }
+ }
>
<h3
className="list-group-item-heading"
key="bar"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/bar"
+ to={
+ Object {
+ "pathname": "/web_api/bar",
+ "query": Object {
+ "internal": true,
+ },
+ }
+ }
>
<h3
className="list-group-item-heading"
key="foo"
onlyActiveOnIndex={false}
style={Object {}}
- to="/web_api/foo"
+ to={
+ Object {
+ "pathname": "/web_api/foo",
+ "query": Object {
+ "query": "Foo",
+ },
+ }
+ }
>
<h3
className="list-group-item-heading"
<SearchBox
onChange={[Function]}
placeholder="api_documentation.search"
+ value=""
/>
</div>
<div
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { memoize } from 'lodash';
import { Domain, Action } from '../../api/web-api';
+import {
+ cleanQuery,
+ RawQuery,
+ serializeString,
+ parseAsOptionalBoolean,
+ parseAsString
+} from '../../helpers/query';
-export function actionsFilter(
- showDeprecated: boolean,
- showInternal: boolean,
- searchQuery: string,
- domain: Domain,
- action: Action
-) {
- const lowSearchQuery = searchQuery.toLowerCase();
+export interface Query {
+ search: string;
+ deprecated: boolean;
+ internal: boolean;
+}
+
+export function actionsFilter(query: Query, domain: Domain, action: Action) {
+ const lowSearchQuery = query.search.toLowerCase();
return (
- (showInternal || !action.internal) &&
- (showDeprecated || !action.deprecatedSince) &&
+ (query.internal || !action.internal) &&
+ (query.deprecated || !action.deprecatedSince) &&
(getActionKey(domain.path, action.key).includes(lowSearchQuery) ||
(action.description || '').toLowerCase().includes(lowSearchQuery))
);
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
+ })
+);