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>
);
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 SAGEBaseapplication
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 theupdateState
function provided by SAGE3. The local state is updated by calling theuseState
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 theuseEffect
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>
);
}
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 appupdate
to resize, move, scale, etc an appdelete
to delete an appupdateState
to update directly the state of an appduplicateApps
to duplicate a list of app instances- a few more internal functions
RoomStore
to access the room collection: list all the rooms andcreate
,update
,delete
functionsBoardStore
to access the board collection: list all the boards andcreate
,update
,delete
functionsUIStore
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 anasset 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));