import { getJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
-export interface Changelog {
- description: string;
- version: string;
-}
-
-export interface Param {
- defaultValue?: string;
- deprecatedKey?: string;
- deprecatedKeySince?: string;
- deprecatedSince?: string;
- description: string;
- exampleValue?: string;
- internal: boolean;
- key: string;
- maximumLength?: number;
- maximumValue?: number;
- maxValuesAllowed?: number;
- minimumLength?: number;
- minimumValue?: number;
- possibleValues?: string[];
- required: boolean;
- since?: string;
-}
-
-export interface Action {
- key: string;
- changelog: Changelog[];
- description: string;
+interface RawDomain {
+ actions: T.WebApi.Action[];
deprecatedSince?: string;
- hasResponseExample: boolean;
- internal: boolean;
- params?: Param[];
- post: boolean;
- since?: string;
-}
-
-export interface Domain {
- actions: Action[];
description: string;
- deprecated: boolean;
internal: boolean;
path: string;
since?: string;
}
-export interface Example {
- example: string;
- format: string;
-}
-
-export function fetchWebApi(showInternal = true): Promise<Domain[]> {
+export function fetchWebApi(showInternal = true): Promise<RawDomain[]> {
return getJSON('/api/webservices/list', { include_internals: showInternal })
- .then(r =>
- r.webServices.map((domain: any) => {
- const deprecated = !domain.actions.find((action: any) => !action.deprecatedSince);
- const internal = !domain.actions.find((action: any) => !action.internal);
- return { ...domain, deprecated, internal };
- })
- )
+ .then(r => r.webServices)
.catch(throwGlobalError);
}
-export function fetchResponseExample(domain: string, action: string): Promise<Example> {
+export function fetchResponseExample(domain: string, action: string): Promise<T.WebApi.Example> {
return getJSON('/api/webservices/response_example', { controller: domain, action }).catch(
throwGlobalError
);
id: string;
success: boolean;
}
+
+ export namespace WebApi {
+ export interface Action {
+ key: string;
+ changelog: Changelog[];
+ description: string;
+ deprecatedSince?: string;
+ hasResponseExample: boolean;
+ internal: boolean;
+ params?: Param[];
+ post: boolean;
+ since?: string;
+ }
+
+ export interface Changelog {
+ description: string;
+ version: string;
+ }
+
+ export interface Domain {
+ actions: Action[];
+ deprecatedSince?: string;
+ description: string;
+ internal?: boolean;
+ path: string;
+ since?: string;
+ }
+
+ export interface Example {
+ example: string;
+ format: string;
+ }
+
+ export interface Param {
+ defaultValue?: string;
+ deprecatedKey?: string;
+ deprecatedKeySince?: string;
+ deprecatedSince?: string;
+ description: string;
+ exampleValue?: string;
+ internal: boolean;
+ key: string;
+ maximumLength?: number;
+ maximumValue?: number;
+ maxValuesAllowed?: number;
+ minimumLength?: number;
+ minimumValue?: number;
+ possibleValues?: string[];
+ required: boolean;
+ since?: string;
+ }
+ }
}
import InternalBadge from './InternalBadge';
import { getActionKey } from '../utils';
import LinkIcon from '../../../components/icons-components/LinkIcon';
-import { Action as ActionType, Domain as DomainType } from '../../../api/web-api';
import { translate, translateWithParameters } from '../../../helpers/l10n';
interface Props {
- action: ActionType;
- domain: DomainType;
+ action: T.WebApi.Action;
+ domain: T.WebApi.Domain;
showDeprecated: boolean;
showInternal: boolean;
}
handleShowParamsClick = (e: React.SyntheticEvent<HTMLElement>) => {
e.preventDefault();
- this.setState({
+ this.setState(state => ({
showChangelog: false,
showResponse: false,
- showParams: !this.state.showParams
- });
+ showParams: !state.showParams
+ }));
};
handleShowResponseClick = (e: React.SyntheticEvent<HTMLElement>) => {
e.preventDefault();
- this.setState({
+ this.setState(state => ({
showChangelog: false,
showParams: false,
- showResponse: !this.state.showResponse
- });
+ showResponse: !state.showResponse
+ }));
};
handleChangelogClick = (e: React.SyntheticEvent<HTMLElement>) => {
e.preventDefault();
- this.setState({
- showChangelog: !this.state.showChangelog,
+ this.setState(state => ({
+ showChangelog: !state.showChangelog,
showParams: false,
showResponse: false
- });
+ }));
};
renderTabs() {
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { Changelog } from '../../../api/web-api';
interface Props {
- changelog: Changelog[];
+ changelog: T.WebApi.Changelog[];
}
export default function ActionChangelog({ changelog }: Props) {
import * as React from 'react';
import Tooltip from '../../../components/controls/Tooltip';
import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { parseVersion } from '../utils';
export default function DeprecatedBadge({ since }: { since?: string }) {
const version = since && parseVersion(since);
</Tooltip>
);
}
-
-function parseVersion(version: string) {
- const match = /(\d+)\.(\d+)/.exec(version);
- if (match) {
- return { major: Number(match[1]), minor: Number(match[2]) };
- } else {
- return undefined;
- }
-}
import DeprecatedBadge from './DeprecatedBadge';
import InternalBadge from './InternalBadge';
import { getActionKey, actionsFilter, Query } from '../utils';
-import { Domain as DomainType } from '../../../api/web-api';
interface Props {
- domain: DomainType;
+ domain: T.WebApi.Domain;
query: Query;
}
<header className="web-api-domain-header">
<h2 className="web-api-domain-title">{domain.path}</h2>
- {domain.deprecated && (
+ {domain.deprecatedSince && (
<span className="spacer-left">
- <DeprecatedBadge />
+ <DeprecatedBadge since={domain.deprecatedSince} />
</span>
)}
import DeprecatedBadge from './DeprecatedBadge';
import InternalBadge from './InternalBadge';
import { isDomainPathActive, actionsFilter, Query, serializeQuery } from '../utils';
-import { Domain } from '../../../api/web-api';
interface Props {
- domains: Domain[];
+ domains: T.WebApi.Domain[];
query: Query;
splat: string;
}
})
.filter(domain => domain.filteredActions.length);
+ const renderDomain = (domain: T.WebApi.Domain) => {
+ const internal = !domain.actions.find(action => !action.internal);
+ return (
+ <Link
+ className={classNames('list-group-item', {
+ active: isDomainPathActive(domain.path, splat)
+ })}
+ key={domain.path}
+ to={{ pathname: '/web_api/' + domain.path, query: serializeQuery(query) }}>
+ <h3 className="list-group-item-heading">
+ {domain.path}
+ {domain.deprecatedSince && <DeprecatedBadge since={domain.deprecatedSince} />}
+ {internal && <InternalBadge />}
+ </h3>
+ </Link>
+ );
+ };
+
return (
<div className="api-documentation-results panel">
- <div className="list-group">
- {filteredDomains.map(domain => (
- <Link
- className={classNames('list-group-item', {
- active: isDomainPathActive(domain.path, splat)
- })}
- key={domain.path}
- to={{ pathname: '/web_api/' + domain.path, query: serializeQuery(query) }}>
- <h3 className="list-group-item-heading">
- {domain.path}
- {domain.deprecated && <DeprecatedBadge />}
- {domain.internal && <InternalBadge />}
- </h3>
- </Link>
- ))}
- </div>
+ <div className="list-group">{filteredDomains.map(renderDomain)}</div>
</div>
);
}
import * as React from 'react';
import InternalBadge from './InternalBadge';
import DeprecatedBadge from './DeprecatedBadge';
-import { Param } from '../../../api/web-api';
import { translate, translateWithParameters } from '../../../helpers/l10n';
interface Props {
- params: Param[];
+ params: T.WebApi.Param[];
showDeprecated: boolean;
showInternal: boolean;
}
export default class Params extends React.PureComponent<Props> {
- renderKey(param: Param) {
+ renderKey(param: T.WebApi.Param) {
return (
<td className="markdown" style={{ width: 180 }}>
<code>{param.key}</code>
);
}
- renderConstraint(param: Param, field: keyof Param, label: string) {
+ renderConstraint(param: T.WebApi.Param, field: keyof T.WebApi.Param, label: string) {
const value = param[field];
if (value !== undefined) {
return (
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import {
- Action,
- Domain,
- Example,
- fetchResponseExample as fetchResponseExampleApi
-} from '../../../api/web-api';
+import { fetchResponseExample as fetchResponseExampleApi } from '../../../api/web-api';
interface Props {
- action: Action;
- domain: Domain;
+ action: T.WebApi.Action;
+ domain: T.WebApi.Domain;
}
interface State {
- responseExample?: Example;
+ responseExample?: T.WebApi.Example;
}
export default class ResponseExample extends React.PureComponent<Props, State> {
import * as React from 'react';
import Helmet from 'react-helmet';
import { Link, withRouter, WithRouterProps } from 'react-router';
+import { maxBy } from 'lodash';
import Domain from './Domain';
import Menu from './Menu';
import Search from './Search';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import { Domain as DomainType, fetchWebApi } from '../../../api/web-api';
-import { getActionKey, isDomainPathActive, Query, serializeQuery, parseQuery } from '../utils';
+import { fetchWebApi } from '../../../api/web-api';
+import {
+ getActionKey,
+ isDomainPathActive,
+ Query,
+ serializeQuery,
+ parseQuery,
+ parseVersion
+} from '../utils';
import { translate } from '../../../helpers/l10n';
import { addSideBarClass, removeSideBarClass } from '../../../helpers/pages';
import { scrollToElement } from '../../../helpers/scrolling';
type Props = WithRouterProps;
interface State {
- domains: DomainType[];
+ domains: T.WebApi.Domain[];
}
class WebApiApp extends React.PureComponent<Props, State> {
fetchWebApi().then(
domains => {
if (this.mounted) {
- this.setState({ domains });
+ this.setState({ domains: this.parseDomains(domains) });
}
},
() => {}
);
}
+ parseDomains(domains: any[]): T.WebApi.Domain[] {
+ return domains.map(domain => {
+ const deprecated = getLatestDeprecatedAction(domain);
+ const internal = !domain.actions.find((action: any) => !action.internal);
+ return { ...domain, deprecatedSince: deprecated && deprecated.deprecatedSince, internal };
+ });
+ }
+
scrollToAction = () => {
const splat = this.props.params.splat || '';
const action = document.getElementById(splat);
}
export default withRouter(WebApiApp);
+
+/** Checks if all actions are deprecated, and returns the latest deprecated one */
+function getLatestDeprecatedAction(domain: Pick<T.WebApi.Domain, 'actions'>) {
+ const noVersion = { major: 0, minor: 0 };
+ const allActionsDeprecated = domain.actions.every(
+ ({ deprecatedSince }) => deprecatedSince !== undefined
+ );
+ const latestDeprecation =
+ allActionsDeprecated &&
+ (maxBy(domain.actions, action => {
+ const version = (action.deprecatedSince && parseVersion(action.deprecatedSince)) || noVersion;
+ return version.major * 1024 + version.minor;
+ }) as T.WebApi.Action);
+ return latestDeprecation || undefined;
+}
import { shallow } from 'enzyme';
import Menu from '../Menu';
-const ACTION = {
+const ACTION: T.WebApi.Action = {
key: 'foo',
changelog: [],
description: 'Foo Desc',
internal: false,
post: false
};
-const DOMAIN1 = {
+const DOMAIN1: T.WebApi.Domain = {
actions: [ACTION],
path: 'foo',
- description: 'API Foo',
- deprecated: false,
- internal: false
+ description: 'API Foo'
};
-const DOMAIN2 = {
+const DOMAIN2: T.WebApi.Domain = {
actions: [ACTION],
path: 'bar',
- description: 'API Bar',
- deprecated: false,
- internal: false
+ description: 'API Bar'
};
const PROPS = {
domains: [DOMAIN1, DOMAIN2],
const SEARCH_BAR = { search: 'Bar', deprecated: false, internal: false };
it('should render deprecated domains', () => {
- const domain = {
+ const domain: T.WebApi.Domain = {
...DOMAIN2,
deprecatedSince: '5.0',
actions: [{ ...ACTION, deprecatedSince: '5.0' }]
});
it('should not render deprecated domains', () => {
- const domain = {
+ const domain: T.WebApi.Domain = {
...DOMAIN2,
deprecatedSince: '5.0',
actions: [{ ...ACTION, deprecatedSince: '5.0' }]
});
it('should render internal domains', () => {
- const domain = { ...DOMAIN2, internal: true, actions: [{ ...ACTION, internal: true }] };
+ const domain: T.WebApi.Domain = {
+ ...DOMAIN2,
+ internal: true,
+ actions: [{ ...ACTION, internal: true }]
+ };
const domains = [DOMAIN1, domain];
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 domain: T.WebApi.Domain = {
+ ...DOMAIN2,
+ internal: true,
+ actions: [{ ...ACTION, internal: true }]
+ };
const domains = [DOMAIN1, domain];
expect(shallow(<Menu {...PROPS} domains={domains} />)).toMatchSnapshot();
});
it('should render only domains with an action matching the query', () => {
- const domain = {
+ const domain: T.WebApi.Domain = {
...DOMAIN2,
- actions: [{ ...ACTION, key: 'bar', path: 'bar', description: 'Bar Desc' }]
+ actions: [{ ...ACTION, key: 'bar', description: 'Bar Desc' }]
};
const domains = [DOMAIN1, domain];
expect(shallow(<Menu {...PROPS} domains={domains} query={SEARCH_FOO} />)).toMatchSnapshot();
});
it('should also render domains with an actions description matching the query', () => {
- const domain = {
+ const domain: T.WebApi.Domain = {
...DOMAIN1,
path: 'baz',
description: 'API Baz',
- actions: [{ ...ACTION, key: 'baz', path: 'baz', description: 'barbaz' }]
+ actions: [{ ...ACTION, key: 'baz', description: 'barbaz' }]
};
const domains = [DOMAIN1, DOMAIN2, domain];
expect(shallow(<Menu {...PROPS} domains={domains} query={SEARCH_BAR} />)).toMatchSnapshot();
import * as React from 'react';
import { shallow } from 'enzyme';
import Params from '../Params';
-import { Param } from '../../../../api/web-api';
-const DEFAULT_PARAM = {
+const DEFAULT_PARAM: T.WebApi.Param = {
key: 'foo',
description: 'Foo desc',
internal: false,
});
it('should render different value constraints', () => {
- const param: Param = {
+ const param: T.WebApi.Param = {
...DEFAULT_PARAM,
defaultValue: 'def',
exampleValue: 'foo',
className="list-group-item-heading"
>
bar
+ <DeprecatedBadge
+ since="5.0"
+ />
</h3>
</Link>
</div>
* 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,
internal: boolean;
}
-export function actionsFilter(query: Query, domain: Domain, action: Action) {
+export function actionsFilter(query: Query, domain: T.WebApi.Domain, action: T.WebApi.Action) {
const lowSearchQuery = query.search.toLowerCase();
return (
(query.internal || !action.internal) &&
internal: query.internal || undefined
})
);
+
+export function parseVersion(version: string) {
+ const match = /(\d+)\.(\d+)/.exec(version);
+ if (match) {
+ return { major: Number(match[1]), minor: Number(match[2]) };
+ } else {
+ return undefined;
+ }
+}