Skip to main content

SAGE3 API Usage

Introduction

  • Before you start, make sure you have the SAGE3 backend running. See Integrated Application for more information.
  • Use the application generator to create a new application. See Web development to scaffold a new application with the associated data schema and various source files.
  • The SAGE3 API is a REST/WS API that is used to communicate with the backend. However, most of the functionality is abstracted away in data stores, containing data and functions to operate on specific collections (user, board, rooms, applications, etc).
  • An application is defined by its state (data values stored in the application) and two components, the application window and its toolbar. The state type is specified through an application schema.

Application

Application Components

A SAGE3 application defines two components: the application window defined as AppComponent and the application toolbar defined as ToolbarComponent. The application window is the main component of the application, and is displayed on the board. The application toolbar is displayed below the application window when the application is selected by the user. Both components receive the application state as a React prop and return a React component (JSX.Element). The prop is the full application element from the application collection, and contains the application state, as well as the _id, _createdAt, _updatedAt, _updatedBy and _createdBy fields.

Both the application window and the toolbar must be exported:

function AppComponent(props: App): JSX.Element {
return <> </>;
}

function ToolbarComponent(props: App): JSX.Element {
return <> </>;
}

export default { AppComponent, ToolbarComponent };

State Definition

We use the zod library to define the schema of an application, inside the index.ts file. The zod library is a TypeScript library that allows us to define a schema for an object, and then use the z.infer function to infer the Typescript type of the object. This data type is stored as the state type, and is used throughout the application. The use of the zod library is not mandatory, but it is recommended to use it to ensure that we can export the application state as a schema that can be used by other languages (python, c#, etc). Zod can describe most data types, including arrays, objects, tuples, unions, intersections, and more.

In the same file, we define the default values for the state object, which is useful for initializing the state of the application. The name of the application is also exported, and is used to identify the application instances.

The four values (schema, state, init and name) are mandatory and must be exported. The application generator updates the SAGE3 internal structures to include your application. The list of applications is stored in the apps.json file, and the application values are added to the files apps.ts, types.ts, and initialValues.ts.

Here is an example of the index.ts file, for an application called Counter with a state variable count defined as a number with an initial value of 42:

import { z } from "zod";

export const schema = z.object({
count: z.number(),
});
export type state = z.infer<typeof schema>;

export const init: Partial<state> = {
count: 42,
};

export const name = "Counter";

An application schema consists of the state of the application as you defined it, and several other values that are used by the SAGE3 backend to identify the application, and to position it on the board. The schema is defined as follows:

export type AppSchema = {
title: string;
roomId: string;
boardId: string;
position: Position;
size: Size;
rotation: Rotation;
type: AppName;
state: AppState; // your application state
raised: boolean;
};

Position, Rotation and Size are defined by SAGE3 internally:

export const PositionSchema = z.object({ x: z.number(), y: z.number(), z: z.number() });
export const RotationSchema = z.object({ x: z.number(), y: z.number(), z: z.number() });
export const SizeSchema = z.object({ width: z.number(), height: z.number(), depth: z.number() });

Each application instance is added to the SAGEBase application collection. Every element in a SAGEBase collection also contains the following fields, added and managed by SAGE3:

export const SBSchema = z.object({
_id: z.string(),
_createdAt: z.number(),
_updatedAt: z.number(),
_updatedBy: z.string(),
_createdBy: z.string(),
});

As a developer, you can read the values of the _id, _createdAt, _updatedAt, _updatedBy and _createdBy fields, but you should not modify them. The _id field is used to identify the application instance, and is used by the SAGE3 backend to identify the application instance. The _createdAt, _updatedAt, _updatedBy and _createdBy fields are used to track the history of the application instance, and are used by the SAGE3 backend to track the history of the application instance.

Using the state

Usually, we extract the state of the application from the props at the start of each component:

function AppComponent(props: App): JSX.Element {
const s = props.data.state as AppState;

The variable s is then used to access the application state and is typed according to your application schema. For instance in the Counter application, we can access the count variable as a number as follows (you should not need to cast the type and you can inspect the type by hovering over the variable name in Visual Studio Code):

function AppComponent(props: App): JSX.Element {
const s = props.data.state as AppState;
const count = s.count;

The count variable can be used in React to define the UI of your application (both in the application window and the application toolbar). The AppWindow component is a generic container provided by SAGE3 to handle the application window functionalities (move, resize, scale, etc). In SAGE3, we use the Chakra UI library to define the UI of the application and its styling (Box and Text here). But any UI library can be used. Notice that we pass the state of the application as a prop to the AppWindow component. This is used by the AppWindow component to update the state of the application when the user moves, resizes, etc the application window. Also, we try to keep the application layout responsive and the font size large enough to be readable on a large screen.

return (
<AppWindow app={props}>
<Box width="100%" height="100%" display="flex" alignItems="center" justifyContent="center">
<Text fontSize="5xl">Count: {count}</Text>
</Box>
</AppWindow>
);

Screenshot 2023-03-14 at 6 39 12 PM

Important: do not confuse the SAGE3 state of an application instance and a local state that you can maintain using React (with useState > for instance). The SAGE3 state is stored in the SAGEBase application collection and is shared by all the SAGE3 clients. The local state > is maintained by React and is not shared by the other SAGE3 clients. The SAGE3 state is updated by calling the updateState function provided by SAGE3. The local state is updated by calling the useState function provided by React. This is particularly important when you want to update the state of the application from an input element (text box, slider, etc). It is very easy to create endless cycles of > update. For instance, if you update the state of the application from an input element, the application will be re-rendered and the input > element will be updated with the new state of the application. This will trigger another update of the state of the application, and so on. To avoid this, you should use the useEffect hook to update the state of the application only when the value of the input element changes.

Update the state

The state of the application is updated by calling the updateState function provided by SAGE3. The updateState function takes the _id of the application instance, and the new state of the application. The new state is merged with the existing state of the application (specify only the variables that you want to change). The updateState function is defined in the AppStore and can be accessed as follows:

const updateState = useAppStore((state) => state.updateState);

The AppStore will handle the communication with the SAGE3 backend to update the state of the application and all the clients will receive the update automatically. React will then re-render the application window and the toolbar.

For instance, in the Counter application, we can update the count variable as follows:

const handleSubClick = () => {
updateState(props.data._id, { count: s.count - 1 });
};

To initialize your application when it starts, you can use the useEffect React hook with an empty dependency array:

  useEffect(() => {
// The application has started
...
}, []);

To respond to state value change, you can use useEffect React hook:

 useEffect(() => {
// The assetid has changed
...
}, [props.data.state.assetid])

Toolbar

You can decide to put some of the UI of your application in the toolbar. Usually we only keep the most used functions in the toolbar. The rest of the UI can be rendered inside the application window. It is up to you. We usually only put a few buttons in the toolbar. The toolbar is displayed below the application window when the application is selected by the user. A few buttons are added automatically by SAGE3 to handle the application window functionalities (move, resize, scale, etc). The toolbar component is defined as follows:

function ToolbarComponent(props: App): JSX.Element {
const s = props.data.state as AppState;

const handleSubClick = () => { ... };
const handleAddClick = () => { ... };

return (
<ButtonGroup isAttached size="xs" colorScheme="teal">
<Tooltip placement="top-start" hasArrow={true} label={'Decrease Count'} openDelay={400}>
<Button onClick={handleSubClick} colorScheme="red">
<MdRemove />
</Button>
</Tooltip>
<Tooltip placement="top-start" hasArrow={true} label={'Increase Count'} openDelay={400}>
<Button onClick={handleAddClick}>
<MdAdd />
</Button>
</Tooltip>
</ButtonGroup>
);
}

Screenshot 2023-03-14 at 6 40 12 PM

Data Stores

We defined a series of data stores to handle most the the collections in the SAGEBase database. The data stores are defined in the webstack/libs/frontend/src/lib/stores folder. The data stores are used to access the data in the SAGEBase database and do not contain any React component (pure Typescript files).

The data stores list contains the following stores:

  • AppStore to access the application collection: contains the list of apps and functions to operate on the apps:
    • create to create a new app
    • update to resize, move, scale, etc an app
    • delete to delete an app
    • updateState to update directly the state of an app
    • duplicateApps to duplicate a list of app instances
    • a few more internal functions
  • RoomStore to access the room collection: list all the rooms and create, update, delete functions
  • BoardStore to access the board collection: list all the boards and create, update, delete functions
  • UIStore to access the UI of the current board: large collection of values such as scale and size of the board, the current board position, selected apps, and UI controls.
  • AssetStore to access all the assets managed in SAGE3: application receiving an asset id can retrieve all the information about the asset (name, type, url, etc).
  • UsersStore to access the user collection: list all the registered users and their settings (full name, nickname, profile, role, type),
  • PresenceStore to access the presence collection: list all the currently active users and their settings (current board, cursor position, viewport, etc).
  • SAGE3 internal collections: plugin, twilio, panel, message, etc.

Follows the description of a few common operations exposed by the application, UI, and asset stores.

Application Store

We saw previously how to update the state of an application. Another very common operation is to create a new application instance. The create function of the AppStore handles that. For instance, you might want to create a stickie note next to an application or to open a webview to show web content.

First, retrieve the create function from the AppStore:

const createApp = useAppStore((state) => state.create);

Then, call the createApp function with the appropriate parameters (refer to the AppSchema to know the list of values that can be passed). For instance, to create a new stickie note::

    // Get information about the current application
const state = props.data.state as AppState;
const pos = props.data.position;
const size = props.data.size;

// Create a new stickie note next to the current application (same room and board and color)
createApp({
title: 'My new stickie',
roomId: props.roomId,
boardId: props.roomId,
position: { x: pos.x + size.width + 20, y: pos.y, z: 0 },
size: { width: size.width, height: size.height, depth: 0 },
rotation: { x: 0, y: 0, z: 0 },
type: 'Stickie',
state: { text: 'Some Text', color: state.color},
raised: true,
});

UIStore Store

The UIStore contains a large collection of values that are used to control the UI of the applications and the current board. The UIStore is used by many internal components, including the AppWindow component to render the application windows and the toolbars.

The UIStore is defined in webstack/libs/frontend/src/lib/stores/ui.ts. It contains:

  • scale, size, z-index, and position of the board
  • UI settings: show/hide the UI, show/hide the app title, lock the board, etc
  • Whiteboard settings: whiteboard mode, clear markers, clear all markers, marker color, etc
  • Lasso settings: selected apps, lasso mode, lasso selection, lasso selection rectangle, etc
  • Toolbar and context menu positions
  • etc

For instance:

  // Retrieve the scale of the board (scale: number)
const scale = useUIStore((state) => state.scale);
// Get the current board current position (x: number, y:number)
const boardPosition = useUIStore((state) => state.boardPosition);

Asset Store

Get all the assets:

  // Get all the assets: (assets: Asset[])
const assets = useAssetStore((state) => state.assets);

The asset schema is defined as follow:

const schema = z.object({
file: z.string(),
owner: z.string(),
room: z.string(),
originalfilename: z.string(),
path: z.string(),
dateCreated: z.string(),
dateAdded: z.string(),
mimetype: z.string(),
destination: z.string(),
size: z.number(),
metadata: z.string().optional(),
derived: z.union([ExtraImageSchema, ExtraPDFSchema]).optional(),
});
  • file: unique filename based on a UUID
  • owner: who uploaded the file
  • room: upload room
  • originalfilename: filename at upload time
  • path: directory
  • dateCreated, dateAdded: dates
  • mimetype: file type in mime standard
  • destination: folder
  • size: size in bytes
  • metadata: contains the name of the JSON metadata file in the server. The data is extracted with EXIFTool after upload.
  • derived: contains either data for the ImageViewer or the PDFViewer application.
// information for derived images
export const ExtraImageSchema = z.object({
fullSize: z.string(),
width: z.number(),
height: z.number(),
aspectRatio: z.number(),
filename: z.string(),
url: z.string(),
sizes: z.array(ImageInfoSchema), // multiple resolutions
});

// Each image resolution
export const ImageInfoSchema = z.object({
url: z.string(),
format: z.string(),
size: z.number(),
width: z.number(),
height: z.number(),
channels: z.number(),
premultiplied: z.boolean(),
});

// Information for PDF file: array of pages with array of images (each page at multiple resolutions)
export const ExtraPDFSchema = z.array(z.array(ImageInfoSchema));