Skip to content

SDK Reference

useSdk()

From any component of a feature page, at any depth:

import React, { useEffect, useState } from 'react';
import { Button } from 'antd';
import { useSdk } from '@clarive/sdk';

const Greeting = ({ idProject }) => {
    const { api, _, user, can, url, navigate } = useSdk();
    const [data, setData] = useState(null);

    useEffect(() => {
        let alive = true;
        api.get('greeting', { id_project: idProject })
            .then(res => alive && setData(res))
            .catch(err => alive && console.error(err.status, err.message));
        return () => { alive = false; };
    }, [idProject]);

    return (
        <div>
            <p>{_('User: %1 (%2)', user.realname || user.username, user.username)}</p>
            <a href={url('/hello-data/detail', { idProject })}>{_('Detail')}</a>
            {can('action.hello_react.admin') && (
                <Button onClick={() => navigate('/code/hello-admin', { idProject })}>Admin</Button>
            )}
        </div>
    );
};

useSdk() only works inside something the core painted for the feature: a page registered with addRoute, or a component registered with addComponent and painted in a core screen. Anywhere else it throws useSdk() must be used inside a feature page or component. Each feature gets its own sdk: api points to its backend and _ to its translations.

useSdk and version can also be imported from @clarive/sdk.

The sdk object

Member Description
version SDK version of the core, e.g. '1.0.0'.
feature Feature data (id, name, entry, css, sdk, i18n).
addRoute(path, render, { permission }) Register a page. See Building the Frontend.
addSideMenuItem(sectionUrl, item, { contexts }) Add an entry to an existing side menu section.
addTopMenuItem(item) Add an entry to the top menu.
addSideMenuSection(section) Add an own side menu section.
addComponent(name, Component) Offer a component to the core screens that accept one (see below). Its full name is <id>/<name>. React.lazy components are accepted.
api.get(path, data) GET /feature/<id>/api/<path>, data as query string.
api.post(path, data) POST, data as a form (it arrives in $c->req->params).
api.postJSON(path, data, { timeout }) POST with a JSON body; timeout in ms.
pluginApi(id) The same against a JS plugin: pluginApi('foo').post('getItems', data) sends POST /plugin/foo with path=getItems.
_(msgid, ...args) Translation with %1..%n placeholders.
language Session language ('es', 'en'...).
user Frozen copy of the user: mid, username, realname, isRoot, language, dateFormat, timeFormat.
can(permission) Does the user have this permission? Only for showing or hiding UI.
navigate(path, query) Navigate to an application url.
url(path, query) Url for an <a href>: url('/hello-data/detail', { id: 3 }) gives '#/hello-data/detail?id=3'.
subscribe(events, callback, { filter }) Listen to server events (see below). Returns the function that stops listening.
openTopic(mid) Open a topic in its tab.
download(url, params) Download a file from a GET url of the application with params. The browser saves it and the page stays.

What is not there, on purpose: the core DataStore, mobx, the router, Cla.* or the core request library. The sdk is the contract: plain, stable values. When a feature needs something more, a new method is added to the sdk (a minor version), instead of opening access to the core internals.

user is taken at startup and does not change while the application is open. Impersonating another user reloads the page, and then user is the impersonated user.

Backend calls

  • Paths are relative: 'greeting', 'items/list'. No leading /, no . or .., and no query string in the path (parameters go in data). An invalid path rejects the promise with a TypeError.
  • They return native promises. Underneath they use the core transport, so the session, re-login after a 401, the impersonated user and the loading indicator work as in the core.
  • An error is an Error with { status, body, url }. message is the msg (or message) of the Clarive error JSON; body is the parsed JSON, or the text if it was not JSON.
api.post('save', { name })
    .then(res => ...)
    .catch(err => {
        if (err.status === 403) ...   // no permission
        console.error(err.message, err.body);
    });

Server events

subscribe listens to the same event stream the core uses (event.topic.modify, event.revision.update, the events fired by your backend...):

const { subscribe } = useSdk();

useEffect(
    () => subscribe('event.revision', (eventKey, events) => refresh(), { filter: { mid: revision.mid } }),
    [revision.mid]
);
  • events is an event key or a list of them. A key also matches its sub keys: 'event.revision' gets event.revision.update.
  • filter keeps only the events whose data match, e.g. { mid: topicMid } (a value or a list of values, compared with ===).
  • callback(eventKey, events) gets the matching events. An exception in the callback is logged in the console and does not stop the subscription.
  • It returns the function that stops listening. Return it from useEffect so it is called when the component unmounts.

Components in core screens

Besides its own pages, a feature can offer components that the core paints inside its screens. The core never names a feature: the component name comes from backend data. Today, from the repository CI: its viewer_components method (see Backend).

// src/index.jsx
const RevisionViewer = React.lazy(() => import('./RevisionViewer.jsx'));

export default function register(sdk) {
    sdk.addComponent('RevisionViewer', RevisionViewer); // -> 'hello-react/RevisionViewer'
}

Places that accept components (viewer_components key, and the props the component gets):

Key Where Props
revision Body of a revision in a topic. The header (name, repository, remove) is still painted by the core. revision (revision data), topic { mid, title }, canEdit, isPrintView, refresh() (reloads the topic)
select Selector used to add revisions from a topic. repository { mid, name, icon, collection, contentUrl }, idProject, topic { mid, title }, field { id, name }, onAddRevision(), onClickTopic({ mid }), onBack()
browse Repository page (Code, Repositories), route /code/repositories/component/<name>. repository, idProject, onBack()

The props are plain values and callbacks, never core stores. Inside the component, useSdk() gives the feature's sdk (its api, _, subscribe...).

// src/RevisionViewer.jsx
import React from 'react';
import { Button, Tag } from 'antd';
import { useSdk } from '@clarive/sdk';

export default function RevisionViewer({ revision, topic, canEdit, refresh }) {
    const { _, openTopic } = useSdk();

    return (
        <div>
            <p>{_('Revision %1 of topic %2', revision.name, topic.title)}</p>
            <Tag>{canEdit ? _('can edit') : _('read only')}</Tag>
            <Button onClick={() => openTopic(topic.mid)}>{_('Open in a tab')}</Button>
            <Button onClick={refresh}>{_('Reload topic')}</Button>
        </div>
    );
}

If the component is not available (the feature is not installed, failed, timed out or does not add it), the core uses its usual view (the view for the repository viewer_type, the generic selector or a notice) and logs [features] component <name> not available (...) in the console. While the feature is still loading, a skeleton is shown. If the component throws while rendering, only that slot shows "The feature X failed to render"; the rest of the screen keeps working.