@colixsystems/widget-sdk 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -29
- package/dist/contract.cjs +137 -110
- package/dist/contract.js +147 -119
- package/dist/hooks.js +734 -571
- package/dist/index.d.ts +228 -65
- package/dist/linter.cjs +56 -0
- package/dist/linter.js +57 -0
- package/dist/manifest.cjs +75 -2
- package/dist/manifest.js +75 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,30 +1,87 @@
|
|
|
1
1
|
# @colixsystems/widget-sdk
|
|
2
2
|
|
|
3
|
-
Common widget interface for [AppStudio](https://github.com/appstudio). This package implements the contract that every widget
|
|
3
|
+
Common widget interface for [AppStudio](https://github.com/appstudio). This package is **core only** — it implements the contract that every widget (built-in or third-party, web or native) speaks: a `WidgetManifest`, a `WidgetContext`, a property schema, the primitives + rendering surface, the helper hooks, events, theme/i18n, and the static linter that gates submissions. **It owns no HTTP and depends on none of the data SDK packages.**
|
|
4
|
+
|
|
5
|
+
The data layer lives in **four separate domain-client packages**, each instantiated by the host and **injected into `WidgetContext`**. Widgets never import those packages — they reach the data surface only through this SDK's hooks, which read the injected client instances:
|
|
6
|
+
|
|
7
|
+
| Injected at | Package | Surface (snake_case verbatim, `list` → `{ data, meta }`) |
|
|
8
|
+
| ----------- | ------- | -------- |
|
|
9
|
+
| `ctx.datastore` | `@colixsystems/datastore-client` | `tables.{list,get}`, `schema(tableId)`, `records(tableId).{ list(query), get(id), create(values), update(id,values) [PATCH], delete(id), aggregate(spec), permissions(recordId).{ list, grant, update, revoke } }` |
|
|
10
|
+
| `ctx.directory` | `@colixsystems/directory-client` | `me()`, `users.{list,get,invite,deactivate,reactivate}`, `groups.{list,create,remove,addMember,removeMember,listMine}`, `invites.{list,revoke,resend}` |
|
|
11
|
+
| `ctx.files` | `@colixsystems/files-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useFile()` resolves |
|
|
12
|
+
| `ctx.payments` | `@colixsystems/payments-client` | `requestPayment(body)`, `getPayment(id)` |
|
|
13
|
+
|
|
14
|
+
**Wire / casing: snake_case end to end.** The clients send and return snake_case **verbatim** (`created_at`, `group_ids`, `can_read`, `amount_cents`, `data_type`, `is_active`, …). There is **no client-side case transform anywhere** — the only transform left in the system is the backend's wire ↔ Prisma middleware. Author-defined record column values pass through verbatim. Every `list(...)` returns the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you.
|
|
15
|
+
|
|
16
|
+
**Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (18 hooks), grouped by the domain client each one reads. **CORE** hooks read host state directly off `WidgetContext` (no data client); the rest delegate to one of the four injected clients. The grouping mirrors the banner sections in [`src/hooks.js`](src/hooks.js).
|
|
17
|
+
|
|
18
|
+
| Group | Hook (signature) | Returns | Reads / scope |
|
|
19
|
+
| ----- | ---------------- | ------- | ------------- |
|
|
20
|
+
| **CORE** | `useTheme()` | `{ colors, spacing, radii, typography }` | `ctx.workspace.theme` — no scope |
|
|
21
|
+
| **CORE** | `useUser()` | `{ id, email, display_name, roles, group_ids }` | `ctx.user` (snake_case verbatim; `id` null when anonymous) — no scope |
|
|
22
|
+
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute }` | `ctx.navigation` — no scope (external URLs use the `Linking` primitive) |
|
|
23
|
+
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope |
|
|
24
|
+
| **CORE** | `useChildRenderer()` | `{ renderNode(node) }` | `ctx.renderer` — no scope (prefer the `WidgetTree` component) |
|
|
25
|
+
| **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
|
|
26
|
+
| **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
|
|
27
|
+
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope |
|
|
28
|
+
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
29
|
+
| **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
|
|
30
|
+
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
31
|
+
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
|
|
32
|
+
| **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
|
|
33
|
+
| **FILES** (`ctx.files`) | `useFile(id)` | `{ url, file, loading, error, refetch }` | `ctx.files.get` — no scope |
|
|
34
|
+
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
35
|
+
| **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (mutations also `users.write:*`) |
|
|
36
|
+
| **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
37
|
+
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser` |
|
|
38
|
+
|
|
39
|
+
All list calls return the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you. There is no `useWorkspace()` or `useLogger()` hook — read the theme via `useTheme()` and the locale via `useI18n()`; the host logger lives on `ctx.logger` (`{ debug, info, warn, error }`).
|
|
40
|
+
|
|
41
|
+
`ctx.recordPermissions`, `ctx.users`, and `ctx.groups` **no longer exist** — they were folded into `ctx.datastore.records(t).permissions` and `ctx.directory.{users,groups}` respectively.
|
|
4
42
|
|
|
5
43
|
See the design reference for the full architecture: [`docs/architecture/widget-marketplace.md`](../../docs/architecture/widget-marketplace.md), specifically section 3.1.
|
|
6
44
|
|
|
7
45
|
## Status
|
|
8
46
|
|
|
9
|
-
`v0.
|
|
47
|
+
`v0.19.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
10
48
|
|
|
11
|
-
### What's new in 0.
|
|
49
|
+
### What's new in 0.19.0
|
|
12
50
|
|
|
13
|
-
|
|
51
|
+
**The data layer splits into four injected domain clients; the SDK becomes core-only.**
|
|
14
52
|
|
|
15
|
-
-
|
|
16
|
-
- **`
|
|
53
|
+
- **The SDK no longer owns any data facade.** The bespoke per-hook facades that used to live on `WidgetContext` (`ctx.datastore` as an opaque host object, `ctx.directory.listUsers`, `ctx.users`, `ctx.groups`, `ctx.recordPermissions`, `ctx.files.get`) are replaced by four host-instantiated, host-injected domain clients: `ctx.datastore` (`@colixsystems/datastore-client`), `ctx.directory` (`@colixsystems/directory-client`), `ctx.files` (`@colixsystems/files-client`, flattened), `ctx.payments` (`@colixsystems/payments-client`). The SDK imports none of them and ships no HTTP.
|
|
54
|
+
- **`ctx.recordPermissions`, `ctx.users`, `ctx.groups` are removed.** Per-record permission management moved under `ctx.datastore.records(tableId).permissions(recordId)`; user / group administration moved under `ctx.directory.users` / `ctx.directory.groups`. The hooks (`useRecordPermissions`, `useUsers`, `useGroups`) keep the same names and signatures — only the client slice they read changed.
|
|
55
|
+
- **snake_case end to end, no client-side transform.** Clients send and return snake_case verbatim (`group_ids`, `can_read`, `is_active`, `amount_cents`, `data_type`, `created_at`, …). The SDK passes bodies straight through and unwraps the `{ data, meta }` list envelope without renaming a single field. Hook return rows and `useUser()` fields are therefore snake_case (`display_name`, `group_ids`, `is_active`, `member_count`, and `useRecordPermissions` rows carry `user_id` / `group_id` / `can_read` / `can_write` / `can_delete` / `can_grant`).
|
|
56
|
+
- **Companion package versions:** `datastore-client 0.5.0`, `files-client 0.3.0`, `directory-client 0.1.0`, `payments-client 0.1.0`.
|
|
57
|
+
- **`CONTRACT.version` → `1.9.0`.** Breaking for `WidgetContext` consumers (removed slices, renamed wire fields); the hook export surface is unchanged.
|
|
58
|
+
|
|
59
|
+
### What was in 0.18.0
|
|
60
|
+
|
|
61
|
+
Two additive features land in this version.
|
|
62
|
+
|
|
63
|
+
**A widget may declare server-side actions in its manifest.**
|
|
64
|
+
|
|
65
|
+
- **`WidgetManifest.actions` is now part of the public contract.** An optional array; each entry is `{ key, name, description?, triggerType, scheduleCron?, timeoutMs?, scriptSource }`. `triggerType` is one of `schedule`, `record_created`, `record_updated`, `record_deleted`; `scheduleCron` is required when `triggerType === "schedule"`. The `scriptSource` (≤ 200 KiB) runs in the **shared isolated-vm action runner** — against `datastore` / `fetch` / `console` / `record` / `tenantId` (the runner surface, **not** the React/SDK widget surface, so SDK imports and hooks are unavailable there and the component linter does not scan it). Actions never run in the rendered app, so they have **no effect on Player ↔ export parity**.
|
|
66
|
+
- **Operators enable actions per tenant** from the Properties Panel. An enabled action materialises a tenant `Action` row **DISABLED** until the operator binds an integration API key (and, for `record_*` triggers, a target table) in the Actions admin page — those bindings are tenant-local, so `triggerTableId` / `apiKeyId` must **not** appear in the manifest (the validator and linter reject them).
|
|
67
|
+
- **New contract fields** `CONTRACT.actionTriggerTypes`, `CONTRACT.actionScriptGlobals`, `CONTRACT.actionScriptMaxBytes` expose the grammar so the Developer page, the AI agent prompt, and `validateManifest` derive it from one source. `validateManifest` now structurally validates `actions`; the marketplace linter rejects malformed / oversized declarations.
|
|
68
|
+
- **New manifest category `ADMINISTRATION`** for app-administration widgets such as User Management. Added to `CONTRACT.manifestCategories`, `validateManifest`, the `WidgetCategory` type, the marketplace category list, and the master-DB `WidgetCategory` enum.
|
|
69
|
+
|
|
70
|
+
**Per-record permission management from inside a widget.**
|
|
71
|
+
|
|
72
|
+
- **`useRecordPermissions(tableId, recordId)` is wired** + the new `WidgetContext.recordPermissions` slice + the new `acl.write:records` scope. Returns `{ permissions, loading, error, grant, revoke, update, refetch }` where `permissions` is `Array<{ id, principalType: "USER" | "GROUP" | "PUBLIC", principalId, canRead, canWrite, canDelete, canGrant }>`. Rejections from `grant` / `revoke` / `update` surface as a structured `PermissionError` (named export) with `code` ∈ `FORBIDDEN | VALIDATION | NOT_FOUND | CONFLICT | INTERNAL`. When `tableId` or `recordId` is null/empty the hook collapses to a stable empty no-op result. Mutating requires `acl.write:records` AND `canGrant` on the target record — Studio owners pass automatically; an APP_USER holds `canGrant` as the record's creator or via a delegated grant. Backs the built-in Chat widget's "+ New channel" + DM-create flows. Additive.
|
|
73
|
+
- **`CONTRACT.version` → `1.8.0`** (additive: a new optional `actions` manifest field, three new contract fields, the `ADMINISTRATION` category, the `useRecordPermissions` hook, the `recordPermissions` context slice, the `acl.write:records` scope, and the `PermissionError` class). No existing export changed signature.
|
|
17
74
|
|
|
18
75
|
### What was in 0.17.0
|
|
19
76
|
|
|
20
|
-
|
|
77
|
+
A runtime schema resolver so widgets can render by column type.
|
|
21
78
|
|
|
22
|
-
- **`useDatastoreSchema(tableId)` is wired** + the new `WidgetContext.datastore.schema` slice. Returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] }` (`null` until loaded).
|
|
79
|
+
- **`useDatastoreSchema(tableId)` is wired** + the new `WidgetContext.datastore.schema` slice. Returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, dataType, required, relationType, targetTableId, isIdentification }] }` (`null` until loaded). Reads structure only, never row data, so a public-grant table resolves for anonymous visitors like a record read. Use it to resolve a stored `columnId` to its name / dataType / relation target at runtime. Reads need the `datastore.read:<table>` scope. Additive.
|
|
23
80
|
- **`CONTRACT.version` → `1.7.0`** (additive: one new hook, one new `datastore.schema` context field). No existing export changed signature.
|
|
24
81
|
|
|
25
82
|
### What's new in 0.16.0
|
|
26
83
|
|
|
27
|
-
|
|
84
|
+
The tenant's **Theme Settings** now flow all the way into `useTheme()`.
|
|
28
85
|
|
|
29
86
|
- **`themeTokens.colors` gains `secondary` + `onSecondary`.** `useTheme().colors.secondary` reflects the tenant's *Secondary Color* picker (with `onSecondary` as its readable contrast color), alongside the existing `primary` / `onPrimary`. Built-in widgets like Button use it for their secondary variant; third-party widgets can use it for a branded second accent. The full `colors` shape is now `{ primary, onPrimary, secondary, onSecondary, surface, onSurface, surfaceMuted, onSurfaceMuted, border, danger, success, warning, info }`.
|
|
30
87
|
- **`colors.primary` / `colors.secondary` / `typography.fontFamily` are tenant-resolved.** The host maps the Studio Theme Settings blob (Primary Color, Secondary Color, Global Font) onto the default tokens before handing them to `useTheme()`, on both the live Player and the exported app — so a widget that reads tokens re-themes automatically. (Custom Google fonts render in the Player today; the exported app falls back to the system face for non-system fonts until font bundling lands.)
|
|
@@ -32,7 +89,7 @@ REQ-THEME — the tenant's **Theme Settings** now flow all the way into `useThem
|
|
|
32
89
|
|
|
33
90
|
### What's new in 0.15.0
|
|
34
91
|
|
|
35
|
-
|
|
92
|
+
The "split-implementation + vetted package list" pivot.
|
|
36
93
|
|
|
37
94
|
- **`CONTRACT.vettedImports` (new).** A curated allowlist of bare specifiers a widget may import — `react`, `@colixsystems/widget-sdk`, `react-native`, `axios`, `date-fns`, `react-native-svg`, `lucide-react-native`, `react-native-maps`, `leaflet`, `react-leaflet`, `expo-av`, `@react-native-community/datetimepicker`, `expo-clipboard`, `expo-haptics`. Each entry carries `platforms` (one or both of `"web"` / `"native"`) and a `category` so the linter and the marketplace listing can render honest platform badges. `CONTRACT.allowedBareImports` (the existing field) is now derived from `vettedImports` and stays a plain `string[]` for back-compat.
|
|
38
95
|
- **`fetch` and `XMLHttpRequest` come off `CONTRACT.bannedApis`.** Widgets may call third-party APIs directly. Calls to the host's own `/api/*` surface will 401 because the JWT token is never shared with widget code; the linter emits a soft `no-host-api-url` warning when it sees host-URL substrings so authors learn the rule statically. Use SDK hooks (`useDatastoreQuery`, `useUsers`, `useFile`, …) for workspace data; use `axios` / `fetch` for third-party APIs.
|
|
@@ -48,12 +105,12 @@ REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. Se
|
|
|
48
105
|
|
|
49
106
|
### What was in 0.14.1
|
|
50
107
|
|
|
51
|
-
- **`groupRef` property type
|
|
108
|
+
- **`groupRef` property type.** Authors can declare `{ type: 'groupRef', label: 'Group' }` in their `propertySchema` to render a Group picker in the Studio Properties Panel. The widget receives a bare `AppUserGroup` UUID, so tenant-copy walks the value transparently. Used by the built-in `appstudio.user-management` widget for its `defaultGroupId` prop and available to third-party widgets that need to anchor behaviour on a specific group.
|
|
52
109
|
- **Patch bump** — additive enumeration entry, no exported function signature changed. `CONTRACT.version` stays `1.4.0`.
|
|
53
110
|
|
|
54
111
|
### What's new in 0.14.0
|
|
55
112
|
|
|
56
|
-
- **`useUsers()` + `useGroups()` — AppUser administration hooks
|
|
113
|
+
- **`useUsers()` + `useGroups()` — AppUser administration hooks.** A widget can invite, deactivate, reactivate, and remove members, and create / delete groups + add / remove members, from a published-app surface. Returns `{ users | groups, loading, error, refetch, ... }` plus imperative mutation methods. Reads gated by `users.read:*` / `groups.read:*`; mutations by `users.write:*` / `groups.write:*`. Rejections surface as a structured `DirectoryError` (new named export) with `code` ∈ `FORBIDDEN | VALIDATION | NOT_FOUND | INVITE_ONLY`. The host signs an `X-Widget-Scopes` header against `JWT_SECRET` so an APP_USER cannot forge a scope set, and the backend additionally gates the request behind a SystemAcl `users.*` / `groups.*` capability grant.
|
|
57
114
|
- **Managing app users from a widget — see the section below.**
|
|
58
115
|
- **New linter rule `no-scope-mismatch-useUsers` / `no-scope-mismatch-useGroups`.** Calling `useUsers().invite()` / `.deactivate()` / `.reactivate()` / `.remove()` without `users.write:*` in the manifest's `requestedScopes` fails the lint; calling `useGroups()` mutation methods without `groups.write:*` fails the lint. Keeps the manifest honest at submission time.
|
|
59
116
|
- **`CONTRACT.version` → `1.4.0`** (additive: two new hooks, two new context slices, six new scope verbs, one new error class). No existing export changed signature.
|
|
@@ -78,12 +135,12 @@ REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. Se
|
|
|
78
135
|
|
|
79
136
|
### What's new in 0.9.0
|
|
80
137
|
|
|
81
|
-
- **`usePayments()` — incoming app-user payments
|
|
138
|
+
- **`usePayments()` — incoming app-user payments.** Returns `{ requestPayment, getPayment }`. `requestPayment({ amountCents, currency?, description, metadata?, returnPath? })` triggers a one-time charge from the signed-in app user and resolves to `{ id, status, checkoutUrl? }`: when `checkoutUrl` is present the widget opens it (web: navigate; native: `expo-web-browser`) — the user pays in **hosted Stripe Checkout**; when absent (the platform's built-in **mock** provider, the default until Stripe is configured) the charge auto-confirms (`status: "PAID"`). `getPayment(id)` polls the terminal status. Backed by a new `WidgetContext.payments` slice and gated by the new `payments.charge:appUser` scope. **No card data ever touches the widget** — never collect card fields yourself. The charge settles to the workspace owner; the amount is bounded by a platform per-charge cap. Rejections are a structured `PaymentError` (also a new named export) with a stable `.code`.
|
|
82
139
|
- **`CONTRACT.version` → `1.2.0`** (additive: one new hook, one new context slice, one new scope, one new error class). No existing export changed signature.
|
|
83
140
|
|
|
84
141
|
### What was in 0.8.0
|
|
85
142
|
|
|
86
|
-
- **`useDirectory()` — read-only user directory hook
|
|
143
|
+
- **`useDirectory()` — read-only user directory hook.** Returns `{ users, loading, error, refetch }` where each user is `{ id, name, role }`. Gated by the new `directory.read:users` scope. Use it for a chat people-list, an @-mention picker, or to resolve an author id to a display name. Player callers get the reduced `{ id, name, role }` projection — email and other admin fields never leave the server for an app end-user. `query` is an optional `{ q, role, isActive, limit, offset }` (`q` substring-matches the display name; `role` is `"USER"` (default), `"INTEGRATION"`, or `"ALL"`). The directory is read-only.
|
|
87
144
|
- **`CONTRACT.version` → `1.1.0`** (additive: one new hook, one new context slice, one new scope). No existing export changed signature.
|
|
88
145
|
|
|
89
146
|
### What was in 0.7.0
|
|
@@ -102,7 +159,7 @@ REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. Se
|
|
|
102
159
|
### What was in 0.5.0
|
|
103
160
|
|
|
104
161
|
- **`TextInput` primitive.** Cross-platform controlled text field — web maps to `<input type="text">` (or `<textarea>` when `multiline` is true), native maps to `react-native` `TextInput`. Props: `value`, `onChangeText`, `placeholder`, `multiline`, `rows`, `disabled`, `style`. Fixes the gap that forced widgets to fall back to raw `<textarea>` (web-only). The AI Widget Agent's system prompt now documents this primitive.
|
|
105
|
-
- **`useDatastoreMutation(...).update(id, partial)` is wired.**
|
|
162
|
+
- **`useDatastoreMutation(...).update(id, partial)` is wired.** Partial-update semantics — only the supplied columns are mutated, the rest of the row is left intact. MANY_TO_MANY relations follow replace-set semantics when supplied; omitting the column leaves the existing links alone. Constraint enforcement runs against the merged row, excluding self so a re-affirm doesn't trip its own UNIQUE.
|
|
106
163
|
|
|
107
164
|
### What was in 0.4.1
|
|
108
165
|
|
|
@@ -129,7 +186,7 @@ REQ-WSDK-PLATFORM — the "split-implementation + vetted package list" pivot. Se
|
|
|
129
186
|
|
|
130
187
|
### What was in 0.3.0
|
|
131
188
|
|
|
132
|
-
Additive: `WidgetManifest` carries an optional `datastoreTemplate` field. When a tenant installs a widget that declares one, the table set is seeded into their workspace alongside the install. Tables follow the
|
|
189
|
+
Additive: `WidgetManifest` carries an optional `datastoreTemplate` field. When a tenant installs a widget that declares one, the table set is seeded into their workspace alongside the install. Tables follow the built-in template semantics (auto-suffixed naming, creator-grants, public grants, cross-relation inheritance) and persist when the widget is later uninstalled. See `WidgetDatastoreTemplate` in `src/index.d.ts` for the constraints — at most 8 tables per widget, 24 columns per table, RELATION columns address siblings by `suffix`.
|
|
133
190
|
|
|
134
191
|
## Public API
|
|
135
192
|
|
|
@@ -139,14 +196,14 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
139
196
|
|
|
140
197
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
141
198
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
142
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email,
|
|
199
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useFile`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, display_name, roles, group_ids }` (snake_case verbatim; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useFile(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
|
|
143
200
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
144
201
|
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`, `DateTimePicker` wraps `@react-native-community/datetimepicker`). The web build aliases `react-native` to `react-native-web` so widgets render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
145
202
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
146
203
|
|
|
147
204
|
## Managing app users from a widget
|
|
148
205
|
|
|
149
|
-
|
|
206
|
+
`useUsers()` and `useGroups()` let a widget invite, deactivate, reactivate, and remove members, plus create / delete groups and add / remove their members. Two gates apply: the manifest must declare the scope (the static analyzer + the host's signed `X-Widget-Scopes` header agree), and the calling APP_USER must hold the matching `users.*` / `groups.*` capability (a SystemAcl grant the Studio admin issues via the Roles UI). A widget that declares `users.write:*` but whose caller lacks the grant gets a `DirectoryError` with `code: 'FORBIDDEN'` — surface that as a "you do not have permission" message.
|
|
150
207
|
|
|
151
208
|
```js
|
|
152
209
|
import { Text, View, Pressable, useUsers, useGroups } from "@colixsystems/widget-sdk";
|
|
@@ -159,7 +216,7 @@ export default function MemberManager() {
|
|
|
159
216
|
<View>
|
|
160
217
|
{users.map((u) => (
|
|
161
218
|
<View key={u.id}>
|
|
162
|
-
<Text>{u.name} — {u.
|
|
219
|
+
<Text>{u.name} — {u.is_active ? "active" : "inactive"}</Text>
|
|
163
220
|
<Pressable onPress={() => deactivate(u.id)}><Text>Deactivate</Text></Pressable>
|
|
164
221
|
<Pressable onPress={() => remove(u.id)}><Text>Remove</Text></Pressable>
|
|
165
222
|
</View>
|
|
@@ -167,7 +224,7 @@ export default function MemberManager() {
|
|
|
167
224
|
<Pressable
|
|
168
225
|
onPress={async () => {
|
|
169
226
|
try {
|
|
170
|
-
await invite({ email: "a@b.com", name: "New User",
|
|
227
|
+
await invite({ email: "a@b.com", name: "New User", group_ids: [groups[0]?.id].filter(Boolean) });
|
|
171
228
|
} catch (err) {
|
|
172
229
|
// err.code is one of FORBIDDEN | VALIDATION | NOT_FOUND | INVITE_ONLY
|
|
173
230
|
}
|
|
@@ -193,7 +250,9 @@ The host rejects calls whose scope is not declared in the manifest (the SDK lint
|
|
|
193
250
|
|
|
194
251
|
## Managing per-record permissions from a widget
|
|
195
252
|
|
|
196
|
-
|
|
253
|
+
`useRecordPermissions(tableId, recordId)` is the in-app surface for sharing a single record with another user or group. The chat widget uses it to invite members into a channel — the channel record's per-record grants ARE the membership list (messages inherit those grants). The hook also covers project-workspace, document-sharing, and team-roster widgets that grant access record-by-record.
|
|
254
|
+
|
|
255
|
+
The rows and bodies are snake_case verbatim. A row carries `user_id` OR `group_id` (both null = a public grant) plus the `can_read` / `can_write` / `can_delete` / `can_grant` flags; the hook reads `ctx.datastore.records(tableId).permissions(recordId)`.
|
|
197
256
|
|
|
198
257
|
```js
|
|
199
258
|
import { Text, View, Pressable, useRecordPermissions, PermissionError } from "@colixsystems/widget-sdk";
|
|
@@ -205,7 +264,7 @@ export default function ShareRecord({ tableId, recordId, partnerUserId }) {
|
|
|
205
264
|
<View>
|
|
206
265
|
{permissions.map((p) => (
|
|
207
266
|
<View key={p.id}>
|
|
208
|
-
<Text>{p.
|
|
267
|
+
<Text>{p.user_id || p.group_id || "public"} — {p.can_write ? "writer" : "reader"}</Text>
|
|
209
268
|
<Pressable onPress={() => revoke(p.id)}><Text>Remove</Text></Pressable>
|
|
210
269
|
</View>
|
|
211
270
|
))}
|
|
@@ -213,15 +272,14 @@ export default function ShareRecord({ tableId, recordId, partnerUserId }) {
|
|
|
213
272
|
onPress={async () => {
|
|
214
273
|
try {
|
|
215
274
|
await grant({
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
canGrant: true,
|
|
275
|
+
user_id: partnerUserId,
|
|
276
|
+
can_read: true,
|
|
277
|
+
can_write: true,
|
|
278
|
+
can_grant: true,
|
|
221
279
|
});
|
|
222
280
|
} catch (err) {
|
|
223
281
|
if (err instanceof PermissionError && err.code === "FORBIDDEN") {
|
|
224
|
-
// The signed-in user lacks
|
|
282
|
+
// The signed-in user lacks can_grant on this record.
|
|
225
283
|
}
|
|
226
284
|
}
|
|
227
285
|
}}
|
|
@@ -242,7 +300,7 @@ The manifest declares the matching scope:
|
|
|
242
300
|
}
|
|
243
301
|
```
|
|
244
302
|
|
|
245
|
-
The server-side gate is `canGrant` on the target record — Studio owners
|
|
303
|
+
The server-side gate is `canGrant` on the target record — Studio owners pass automatically; an APP_USER holds `canGrant` as the record's creator or via a delegated grant. A caller without `canGrant` receives `PermissionError { code: "FORBIDDEN" }`. The hook collapses to a stable no-op when `tableId` or `recordId` is null/empty — so a widget can render its picker first, then bind to the picked record without conditional hook tricks.
|
|
246
304
|
|
|
247
305
|
## Linter
|
|
248
306
|
|