Settings are what gets filled in when an app is installed, like an API token or the URL of a school's environment. Examplary shows them as a form on the app's card, validates them, and passes them to every hook of the app.
app.addSetting({
key: "environment",
label: { en: "Environment" },
type: "select",
options: [
{ value: "eu", label: { en: "Europe" } },
{ value: "us", label: { en: "United States" } },
],
default: "eu",
schema: z.enum(["eu", "us"]),
});Workspace and user settings
Each setting has a scope, which decides who fills it in:
org: an admin, once for the whole workspace, under Settings → Apps & integrations. Use this for things like a school's API key.user: each teacher, for themselves, under Settings → My apps. Use this when every teacher connects their own account, like their Google account.
A setting without a scope uses the app's scope, and org when the app has none either. An app can mix both: the workspace installs the app with its org settings, after which teachers fill in their own. Hooks get both, merged.
A teacher can only fill in their settings once the workspace has installed the app.
Field types
| Type | Shown as |
|---|---|
text | A text input |
password | A text input that hides what's typed |
url | A URL input |
boolean | A switch |
select | A dropdown with the given options |
component | Your own React component, see below |
Other properties of a setting:
| Property | Description |
|---|---|
key | The key its value is stored under, like apiToken. Required. |
label | The field's label. A setting without a label isn't shown: use these for values your hooks set. |
help | Help text shown under the field. |
placeholder | Placeholder text. |
default | The initial value, a string or (for boolean) a boolean. |
schema | A zod schema the value has to match. Fields that accept undefined are optional. |
optional | Whether the field may be left empty, when there's no schema to tell. |
secret | Store the value, but never send it back to the browser. Use this for passwords and tokens. |
scope | org or user, see above. |
Checking and preparing settings
Before saving, Examplary checks the values against each setting's schema. To check more, like whether an API token actually works, add a validate step. It gets the settings and has to return true; throw an error to show its message to the person filling in the form:
app.addSettingsValidateStep(async (settings) => {
const res = await fetch("https://api.acme.com/me", {
headers: { Authorization: `Bearer ${settings.apiToken}` },
});
if (!res.ok) {
throw new Error("Acme didn't accept this API token.");
}
return true;
});A prepare step runs before that, and can turn what was filled in into what you'd like to store. It returns the settings to save, which are then checked as usual. A common use is swapping a one-time sign-in code for a long-lived token, which you then store in a secret setting without a label:
app.addSetting({
key: "refreshToken",
type: "text",
secret: true,
schema: z.string(),
});
app.addSettingsPrepareStep(async (settings) => ({
refreshToken: await exchangeCode(settings.authorizationCode),
}));To clean up when a workspace removes your app, add an uninstall step. It gets the workspace's settings; if it throws, the app stays installed and the admin sees your message:
app.addUninstallStep(async (settings) => {
await disconnectWorkspace(settings.apiToken);
});Component settings
When a form field isn't enough, like for a "Sign in with…" button that opens a popup, a setting can render your own component:
app.addSetting({
key: "authorizationCode",
label: { en: "Acme account" },
type: "component",
component: {
path: "./components/connect-button.tsx",
translations: {
connect: { en: "Connect your Acme account" },
},
},
schema: z.string().optional(),
});The component gets setValue(key, value) to set the value of any of the app's settings, and submit() to save them. A card with a component setting has no save button of its own, so the component has to call submit():
import { Button, FrontendSettingsFieldComponent } from "@examplary/ui";
const ConnectButton: FrontendSettingsFieldComponent = ({
t,
setValue,
submit,
}) => (
<Button
onClick={async () => {
setValue("authorizationCode", await signInWithAcme());
await submit();
}}
>
{t("connect")}
</Button>
);
export default ConnectButton;Buttons on the app's card
A SettingsButton adds a button to the app's card, for admins. Its run hook gets the workspace's settings, and the message of an error it throws is shown to the admin:
import { SettingsButton } from "@examplary/apps";
app.addCapability(
new SettingsButton({
id: "sync",
label: { en: "Sync now" },
run: async (settings) => {
await startSync(settings.apiToken);
},
}),
);