@colixsystems/widget-sdk 0.192.0 → 0.194.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 +36 -38
- package/dist/contract.cjs +30 -33
- package/dist/contract.js +30 -33
- package/dist/hooks.js +15 -16
- package/dist/index.d.ts +15 -29
- package/package.json +1 -1
- package/src/contract.js +30 -33
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
53
53
|
| **DATASTORE** | `useInterpretDraft(tableId)` | `{ interpret, interpreting, error, result, available }` | `interpret(tableId, body)` — `datastore.read:<table>`. Turns ONE sentence a user typed ("walk at 11 am tomorrow") into DRAFT column values so a form can prefill itself. IMPERATIVE: call `interpret(text, { fields?, timeZone? })` from an event handler, never on mount. It DRAFTS and writes nothing — show the values for review, then submit through `useDatastoreMutation().create`. Resolves to `{ values, unresolved }`; `values` is keyed by column NAME (the shape `create()` takes) and `unresolved` names the fields the sentence did not state. Only text / number / boolean / date / datetime / array columns are drafted — `FILE`, `RELATION`, `USER` and `USER_GROUP` carry ids and are never guessed. Fails closed to an empty draft. **Every call spends the workspace's AI credits** and is rate-limited per actor, so call it once per user action (never on mount or in a render loop); once the workspace runs out the call is refused with a generic 429 — an app user is deliberately **not** told the workspace's billing state, since they have never heard of an AI credit and cannot buy one. Never surface a raw error to the person filling the form: say drafting is unavailable and keep every field editable by hand. `available` is false where the host brokers no interpreter. |
|
|
54
54
|
| **AGENTS** | `useAgent(agentRef)` | `{ send, cancel, reset, messages, sending, resuming, error, available }` | `ctx.agents` — `ai.invoke:*`. Hold a conversation with one of the WORKSPACE's own AI agents (an author-written name + system prompt). Sending is IMPERATIVE: call `send(text)` from an event handler, never on mount — the first send opens the conversation, so a widget nobody types into costs nothing. On mount the hook DOES reopen the caller's most recent thread with this agent, so a refresh or a navigation keeps the conversation instead of starting a blank one; that resume only READS and spends no credit. `resuming` is true while it runs — render a loading state, not an empty thread. `messages` is the running transcript, oldest first. You **cannot** set or read the system prompt, replay an edited transcript, or pick a model: the server assembles every turn from the stored prompt plus the stored history, and `content` is the only thing that reaches the wire. `reset()` drops the thread (a "new chat" affordance), and the new thread is the one a later mount resumes. **Every send spends the workspace's AI credits** and is rate-limited per caller AND per workspace. Rejects with `AgentError`; branch on `.code` — `AI_QUOTA_EXCEEDED` (429) is the workspace's credit ceiling and will NOT clear on a retry, unlike `RATE_LIMITED`, so never offer "try again" for it, and `AGENT_DISABLED` (409) means the author turned it off, so hide the affordance. Never surface a raw error or the workspace's billing state to an app user. `available` is false when no agent is bound or the host brokers none — render a visible unbound state, never a blank box. An assistant message that used a tool also carries **`activity`** — what the agent DID with the user's data: the table it read, or the table and columns it CHANGED (never the values, and never a row the permissions refused). **Render a write.** A reply saying "done!" is not evidence a row was written, and if the agent got it wrong the user has no other way to notice. A talk-only turn omits the key entirely. |
|
|
55
55
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*`. **To CLEAR a cell, pass it empty** — `update(id, { when: null })` (also `""` or `[]`) empties that cell and it reads back as `null` (`[]` on an array column). There is no separate clear call, so never write a sentinel such as `0` or a placeholder date to mean "no value". `false` and `0` are values, not clears, and a REQUIRED column cannot be cleared. |
|
|
56
|
-
| **DATASTORE** | `useDatastoreSubscription(table, handlers, options)` (optional: options) | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
|
|
56
|
+
| **DATASTORE** | `useDatastoreSubscription(table, handlers, options)` (optional: options) | `{ status }` — `"connecting" \| "live" \| "reconnecting" \| "fallback"` | `records(table).subscribe` — `datastore.read:<table>`. Live `onCreated` / `onUpdated` / `onDeleted` off the REQ-RT-07 socket; never throws, resolving to `{ status: "fallback" }` so the widget polls instead. A whole-table subscribe is gated on read-EVERY-row, because one envelope reaches every subscriber of the table — so for a table governed by per-record grants pass `options.scope`: `{ kind: "record", record_id }` for one row, or `{ kind: "parent", relation_column, record_id }` for the rows whose RELATION column points at that parent (the column must carry `inheritAcl`, else the subscribe reports `"fallback"`). For a live list of the signed-in user's own rows pass `{ kind: "granted" }`: every row they read through a grant to them or one of their groups, plus `onAccessGained(record)` / `onAccessLost({ id })` as rows enter and leave that set. Re-subscribes on the scope's VALUES, so a fresh object literal each render is fine. |
|
|
57
57
|
| **DATASTORE** | `useTyping(tableId, recordId)` | `{ typingUserIds, setTyping, status }` — status `"connecting" \| "live" \| "unavailable"` | `records(table).typing` — `datastore.read:<table>`. Who else is typing in one record, over the same socket as `useDatastoreSubscription`. Call `setTyping(true)` on every keystroke (throttled to one signal per 3 s; `false` is sent after 5 s idle) and `setTyping(false)` on send, clear or blur. `typingUserIds` lists the OTHER users' ids; each drops out 6 s after their last signal. Needs a signed-in user who can read the record. Never throws: an anonymous visitor, a refused subscription or a host whose client lacks `typing` reads `{ typingUserIds: [], status: "unavailable" }`. Re-subscribes when either id changes. |
|
|
58
58
|
| **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) |
|
|
59
59
|
| **DATASTORE** | `useCanWrite(tableId, options)` (optional: options) | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "may this caller write", reading the same table-ACL answer the write endpoint enforces — so a table granting Create to Everyone answers `true` for a logged-out visitor, and this hook alone is the right gate for a widget meant to work without signing in. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
|
|
@@ -62,7 +62,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
62
62
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query)` (optional: query) | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
63
63
|
| **DIRECTORY** | `useUsers(query)` (optional: query) | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove, sendPasswordReset }` | `directory.users.*` — `users.read:*` plus the `users.read` (or `users.write`) capability, else `FORBIDDEN` (edits, incl. `sendPasswordReset()`, also `users.write:*`; `remove()` also `users.delete:*`) |
|
|
64
64
|
| **DIRECTORY** | `useGroups(query)` (optional: query) | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
65
|
-
| **DIRECTORY** | `useInvites(query)` (optional: query) | `{ invites, loading, error, refetch, resend, revoke }` | `directory.invites.*` — `users.write:*` + the SystemAcl `users.write` capability (the whole invite surface, list included). `query` is `{ status?, q?, app_user_id?, limit?, offset? }` with `status` ∈ `pending \| accepted \| revoked \| expired \|
|
|
65
|
+
| **DIRECTORY** | `useInvites(query)` (optional: query) | `{ invites, loading, error, refetch, resend, revoke }` | `directory.invites.*` — `users.write:*` + the SystemAcl `users.write` capability (the whole invite surface, list included). `query` is `{ status?, q?, app_user_id?, limit?, offset? }` with `status` ∈ `pending \| accepted \| revoked \| expired \| open \| all` (endpoint default `all`; `open` = `pending` or `expired`); `q` matches the invite's name or email, `app_user_id` returns the invites reserving that user id. |
|
|
66
66
|
| **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
|
|
67
67
|
| **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId })` (optional: spaceType, folderId) | `{ openPdf, opening, canOpenPdf, exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.renderPdf` + `ctx.device.pdfViewer`, `ctx.filestore.files.exportPdf` — `files.write:*`. `openPdf(html, { fileName? })` renders the HTML to a PDF server-side and OPENS it (web: PDF tab; native: print / share sheet) without saving. `exportToPdf(html, { fileName?, folderId? })` renders it and saves it as a file (`application/pdf`). Same server-side renderer on web + native. |
|
|
68
68
|
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser`. Rejects with `PaymentError { code, message, retryable }`; when `retryable` is `false` show `message` and drop the retry. Charges are accepted ONLY in the currency the workspace sells in — omit `currency` and the platform applies it (a disagreeing literal is a publish-blocking `payment-currency` finding). |
|
|
@@ -77,7 +77,33 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
77
77
|
|
|
78
78
|
## Status
|
|
79
79
|
|
|
80
|
-
`v0.
|
|
80
|
+
`v0.194.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**.
|
|
81
|
+
|
|
82
|
+
### What's new in 0.194.0 (contract 1.155.0)
|
|
83
|
+
|
|
84
|
+
**Send-later invitations are withdrawn.** 0.183.0 let `useUsers().invite()` take `send_invitation: false` to store an invitation without emailing it. That argument, the `not_sent` invite status and the `sent_at` field are removed: `invite()` emails the invitation at once, as it did before, and `useInvites().resend(inviteId)` re-sends a pending one. A widget that wants to line people up first keeps its own list and calls `invite()` when it is ready.
|
|
85
|
+
|
|
86
|
+
### What's new in 0.193.0 (contract 1.154.0)
|
|
87
|
+
|
|
88
|
+
**`useDatastoreSubscription` can keep a list of the user's own rows live.** Pass `{ scope: { kind: "granted" } }` and the hook streams every row of the table the signed-in user can read through a grant to them or to one of their groups — their conversations, their cases. Without it such a list had to poll: a whole-table subscribe needs read on every row, and one record scope per row runs out of subscriptions.
|
|
89
|
+
|
|
90
|
+
Two new handlers report changes to the set itself. `onAccessGained(record)` fires with the full row when a grant starts letting the user read it, for example when they are added to a conversation. `onAccessLost({ id })` fires when they can no longer read it, and no further event for that row arrives. Merge `onCreated` / `onUpdated` / `onAccessGained` into your list by `id`, and remove the row on `onDeleted` or `onAccessLost`:
|
|
91
|
+
|
|
92
|
+
```jsx
|
|
93
|
+
const { data, refetch } = useDatastoreQuery(conversationsTableId);
|
|
94
|
+
const [live, setLive] = useState([]);
|
|
95
|
+
useEffect(() => setLive(data), [data]);
|
|
96
|
+
const upsert = (row) => setLive((list) => [row, ...list.filter((r) => r.id !== row.id)]);
|
|
97
|
+
const drop = ({ id }) => setLive((list) => list.filter((r) => r.id !== id));
|
|
98
|
+
const { status } = useDatastoreSubscription(
|
|
99
|
+
conversationsTableId,
|
|
100
|
+
{ onCreated: upsert, onUpdated: upsert, onAccessGained: upsert, onDeleted: drop, onAccessLost: drop },
|
|
101
|
+
{ scope: { kind: "granted" } },
|
|
102
|
+
);
|
|
103
|
+
// Poll with refetch() only while status === "fallback".
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
A logged-out visitor's granted subscribe resolves to `{ status: "fallback" }`. Backed by datastore-client 0.21.0. Additive; works identically in the web Player and the native export.
|
|
81
107
|
|
|
82
108
|
### What's new in 0.192.0 (contract 1.153.0)
|
|
83
109
|
|
|
@@ -228,24 +254,7 @@ An installed widget is not rebuilt or republished for this. The web Player hands
|
|
|
228
254
|
|
|
229
255
|
### What's new in 0.183.0 (contract 1.144.0)
|
|
230
256
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
```js
|
|
234
|
-
const { invite } = useUsers();
|
|
235
|
-
const { invites, resend } = useInvites({ status: "not_sent" });
|
|
236
|
-
|
|
237
|
-
const prepared = await invite({ email: "bo@example.com", name: "Bo", send_invitation: false });
|
|
238
|
-
// prepared.status === "not_sent", prepared.sent_at === null — nobody was emailed.
|
|
239
|
-
|
|
240
|
-
await resend(prepared.id); // emails it; the row is now "pending" with a fresh 7-day expiry
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
- **`send_invitation`** defaults to `true`, so an existing `invite({ email, name, group_ids })` call emails at once exactly as before. `false` stores the invitation and emails nobody. A value that is not a boolean rejects with a `DirectoryError` coded `VALIDATION`.
|
|
244
|
-
- **`not_sent` joins the invite `status` values**, and every invite row carries **`sent_at`** (`null` until the first send). A `not_sent` invitation cannot be accepted yet and its `expires_at` means nothing until it is sent, so show no expiry for it. `useInvites({ status: "not_sent" })` lists just the prepared ones. Where several states apply the first of accepted, revoked, not_sent, expired, pending wins.
|
|
245
|
-
- **`useInvites().resend(inviteId)` is the one call that sends**, whether or not the invitation was sent before. There is no separate `send` method: on a `not_sent` row it is the first send, on a `pending` or `expired` one it sends again, and either way it stamps `sent_at`, restarts the expiry and resolves the row as `pending`. Label the button to match the row — "Send invitation" when `status === "not_sent"`, "Resend" otherwise.
|
|
246
|
-
- **A widget that switches on `status` should handle the new value.** A `not_sent` row falls through an `if (status === "pending")` check, so a list that offers its actions only to pending rows would leave a prepared invitation with no way to send it.
|
|
247
|
-
|
|
248
|
-
The built-in User Management widget offers the choice on its invite form ("Send the invitation later", off by default) and a "Send invitation" action on each row that has not been sent. No new scope: both calls sit under `users.write:*`, as before.
|
|
257
|
+
Send-later invitations (`send_invitation`, the `not_sent` status, `sent_at`). Withdrawn in 0.194.0.
|
|
249
258
|
|
|
250
259
|
### What's new in 0.182.0 (contract 1.143.0)
|
|
251
260
|
|
|
@@ -2272,13 +2281,11 @@ The matching manifest declares the scopes:
|
|
|
2272
2281
|
}
|
|
2273
2282
|
```
|
|
2274
2283
|
|
|
2275
|
-
`invite()` emails the invitation at once. Pass `send_invitation: false` to store it without emailing anyone — the row comes back with `status: "not_sent"` and `sent_at: null` — and send it later with `useInvites().resend(inviteId)`.
|
|
2276
|
-
|
|
2277
2284
|
The host rejects calls whose scope is not declared in the manifest (the SDK linter catches this statically too). Declaring a write scope is also a consent prompt the Studio admin sees at install time — the wider the scope set, the more careful the admin is about granting the install.
|
|
2278
2285
|
|
|
2279
2286
|
## Listing pending invitations from a widget
|
|
2280
2287
|
|
|
2281
|
-
`useInvites(query?)` lists the workspace's invites and
|
|
2288
|
+
`useInvites(query?)` lists the workspace's invites and resends / revokes them:
|
|
2282
2289
|
|
|
2283
2290
|
```jsx
|
|
2284
2291
|
import { useInvites, View, Text, Pressable } from '@colixsystems/widget-sdk';
|
|
@@ -2293,9 +2300,7 @@ function PendingInvites() {
|
|
|
2293
2300
|
{invites.map((i) => (
|
|
2294
2301
|
<View key={i.id}>
|
|
2295
2302
|
<Text>{i.email} · {i.status}</Text>
|
|
2296
|
-
<Pressable onPress={() => resend(i.id).catch(() => {})}>
|
|
2297
|
-
<Text>{i.status === 'not_sent' ? 'Send invitation' : 'Resend'}</Text>
|
|
2298
|
-
</Pressable>
|
|
2303
|
+
<Pressable onPress={() => resend(i.id).catch(() => {})}><Text>Resend</Text></Pressable>
|
|
2299
2304
|
<Pressable onPress={() => revoke(i.id).catch(() => {})}><Text>Revoke</Text></Pressable>
|
|
2300
2305
|
</View>
|
|
2301
2306
|
))}
|
|
@@ -2304,16 +2309,9 @@ function PendingInvites() {
|
|
|
2304
2309
|
}
|
|
2305
2310
|
```
|
|
2306
2311
|
|
|
2307
|
-
Rows are snake_case: `{ id, app_user_id, email, name, group_ids, status,
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
than comparing `expires_at` against the device clock.
|
|
2311
|
-
|
|
2312
|
-
A `not_sent` row is an invitation created with `send_invitation: false`: nobody has
|
|
2313
|
-
been emailed and `sent_at` is `null`. `resend(inviteId)` is the one call that sends
|
|
2314
|
-
an invitation, a first send and a repeat send alike — it emails the invitee, stamps
|
|
2315
|
-
`sent_at`, restarts the 7-day expiry and resolves the row as `pending`. Pass
|
|
2316
|
-
`{ status: 'not_sent' }` to list only the prepared ones.
|
|
2312
|
+
Rows are snake_case: `{ id, app_user_id, email, name, group_ids, status, expires_at,
|
|
2313
|
+
accepted_at, revoked_at, created_at }`. Trust the server-computed `status` rather than
|
|
2314
|
+
comparing `expires_at` against the device clock.
|
|
2317
2315
|
|
|
2318
2316
|
`query` also takes `q` (a case-insensitive substring match on the invite's name or
|
|
2319
2317
|
email) and `app_user_id`. Each row's `app_user_id` is the id the invitee's user account
|
|
@@ -2321,7 +2319,7 @@ will have once they accept. A `USER` column may hold that id before the person h
|
|
|
2321
2319
|
joined — it then reads back as `{ id, name, invited: true }`, with no `email` — and
|
|
2322
2320
|
`useInvites({ app_user_id: value.id, status: 'open', limit: 1 })` returns the invite
|
|
2323
2321
|
behind it. `status: 'open'` is every invite that is neither accepted nor revoked
|
|
2324
|
-
(`pending
|
|
2322
|
+
(`pending` or `expired`); a re-invite keeps the id, so without it earlier invites match too.
|
|
2325
2323
|
|
|
2326
2324
|
**The gate is `users.write`, not `users.read`.** A pending invite exposes the email of
|
|
2327
2325
|
someone who is not a member yet, so the backend gates the entire invite surface —
|
package/dist/contract.cjs
CHANGED
|
@@ -1749,9 +1749,8 @@ const HOOKS = [
|
|
|
1749
1749
|
// deactivate / reactivate / sendPasswordReset) additionally need
|
|
1750
1750
|
// `users.write:*`, and the
|
|
1751
1751
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
1752
|
-
// call accepts `{ email, name,
|
|
1753
|
-
//
|
|
1754
|
-
// `send_invitation` is false). Mutating users from
|
|
1752
|
+
// call accepts `{ email, name, groupIds? }` and returns the resulting
|
|
1753
|
+
// AppUserInvite row (the email is sent by the host). Mutating users from
|
|
1755
1754
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
1756
1755
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
1757
1756
|
// that only call read methods need only `users.read:*`.
|
|
@@ -1769,13 +1768,8 @@ const HOOKS = [
|
|
|
1769
1768
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1770
1769
|
"sendPasswordReset) need users.write:*, and the destructive remove() " +
|
|
1771
1770
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
1772
|
-
"{ email, name, group_ids
|
|
1773
|
-
"
|
|
1774
|
-
"The host emails the invitation at once unless send_invitation is " +
|
|
1775
|
-
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
1776
|
-
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
1777
|
-
"later. A send_invitation that is not a boolean rejects with " +
|
|
1778
|
-
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
1771
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
1772
|
+
"(the email is sent by the host). sendPasswordReset(userId) mails the " +
|
|
1779
1773
|
"standard self-serve reset link to that user's own registered address " +
|
|
1780
1774
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
1781
1775
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -1788,7 +1782,7 @@ const HOOKS = [
|
|
|
1788
1782
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1789
1783
|
refetch: "() => Promise<void>",
|
|
1790
1784
|
invite:
|
|
1791
|
-
"({ email, name, group_ids
|
|
1785
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
1792
1786
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1793
1787
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1794
1788
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -1833,8 +1827,7 @@ const HOOKS = [
|
|
|
1833
1827
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
1834
1828
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1835
1829
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1836
|
-
// caller's `users.write` SystemAcl capability.
|
|
1837
|
-
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
1830
|
+
// caller's `users.write` SystemAcl capability.
|
|
1838
1831
|
{
|
|
1839
1832
|
name: "useInvites",
|
|
1840
1833
|
signature: "useInvites(query)",
|
|
@@ -1844,23 +1837,13 @@ const HOOKS = [
|
|
|
1844
1837
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1845
1838
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1846
1839
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1847
|
-
"res.data; rows are snake_case (email, status,
|
|
1848
|
-
"
|
|
1849
|
-
"
|
|
1850
|
-
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
1851
|
-
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
1852
|
-
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
1853
|
-
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
1854
|
-
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
1855
|
-
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
1856
|
-
"'pending'. There is no separate send method; label the button " +
|
|
1857
|
-
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
1858
|
-
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1840
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1841
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1842
|
+
"value. query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1859
1843
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
1860
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
1861
|
-
"
|
|
1862
|
-
"
|
|
1863
|
-
"not_sent or expired). q is a " +
|
|
1844
|
+
"{ status: 'pending' } for the usual outstanding-invite list; " +
|
|
1845
|
+
"status 'open' is every invite neither accepted nor revoked (pending " +
|
|
1846
|
+
"or expired). q is a " +
|
|
1864
1847
|
"case-insensitive substring match on the invite's name or email. " +
|
|
1865
1848
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
1866
1849
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -1877,12 +1860,12 @@ const HOOKS = [
|
|
|
1877
1860
|
"refetch on success.",
|
|
1878
1861
|
returnShape: {
|
|
1879
1862
|
invites:
|
|
1880
|
-
"Array<{ id, app_user_id, email, name, group_ids, status,
|
|
1863
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1881
1864
|
loading: "boolean",
|
|
1882
1865
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1883
1866
|
refetch: "() => Promise<void>",
|
|
1884
1867
|
resend:
|
|
1885
|
-
"(inviteId) => Promise<Invite> //
|
|
1868
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1886
1869
|
revoke:
|
|
1887
1870
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1888
1871
|
},
|
|
@@ -2060,6 +2043,13 @@ const HOOKS = [
|
|
|
2060
2043
|
"table governed by per-record grants, since a scope-less subscribe must " +
|
|
2061
2044
|
"prove read-every-row. A `parent` scope requires the column be an " +
|
|
2062
2045
|
"inheritAcl RELATION, else the subscribe resolves to " +
|
|
2046
|
+
"{ status: 'fallback' }. { kind: 'granted' } streams every row the " +
|
|
2047
|
+
"signed-in user can read through a grant to them or to one of their " +
|
|
2048
|
+
"groups — the way to keep a list of 'my conversations' or 'my cases' " +
|
|
2049
|
+
"live — and adds two handlers: onAccessGained(record) when a row " +
|
|
2050
|
+
"becomes readable (the full row) and onAccessLost({ id }) when it stops " +
|
|
2051
|
+
"being readable, after which no further event for it arrives. A " +
|
|
2052
|
+
"logged-out visitor's granted subscribe resolves to " +
|
|
2063
2053
|
"{ status: 'fallback' }.",
|
|
2064
2054
|
returnShape: {
|
|
2065
2055
|
status:
|
|
@@ -3042,7 +3032,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3042
3032
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
3043
3033
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
3044
3034
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
3045
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites
|
|
3035
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke pending invites, gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:* (destructive user removal by users.delete:*).",
|
|
3046
3036
|
required: true,
|
|
3047
3037
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
3048
3038
|
},
|
|
@@ -5070,7 +5060,14 @@ const CONTRACT = deepFreeze({
|
|
|
5070
5060
|
// `useAudioPlayer(uri)`, backed by the optional `device.audioRecorder` /
|
|
5071
5061
|
// `device.audioPlayer` slices (MediaRecorder + <audio> on web, expo-audio
|
|
5072
5062
|
// on native), with `AudioRecorderError` / `AudioPlayerError`.
|
|
5073
|
-
|
|
5063
|
+
// 1.154.0: additive (sc-10589) — `useDatastoreSubscription` accepts
|
|
5064
|
+
// `scope: { kind: "granted" }` (the rows the signed-in user reads through
|
|
5065
|
+
// a user or group grant) and the `onAccessGained` / `onAccessLost`
|
|
5066
|
+
// handlers that report rows entering and leaving that set.
|
|
5067
|
+
// 1.155.0: narrowing (sc-10566) — withdraws 1.144.0: `useUsers().invite`
|
|
5068
|
+
// takes no `send_invitation` and always emails, invite rows carry no
|
|
5069
|
+
// `sent_at`, and `not_sent` is no longer a status.
|
|
5070
|
+
version: "1.155.0",
|
|
5074
5071
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5075
5072
|
hooks: HOOKS,
|
|
5076
5073
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1749,9 +1749,8 @@ const HOOKS = [
|
|
|
1749
1749
|
// deactivate / reactivate / sendPasswordReset) additionally need
|
|
1750
1750
|
// `users.write:*`, and the
|
|
1751
1751
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
1752
|
-
// call accepts `{ email, name,
|
|
1753
|
-
//
|
|
1754
|
-
// `send_invitation` is false). Mutating users from
|
|
1752
|
+
// call accepts `{ email, name, groupIds? }` and returns the resulting
|
|
1753
|
+
// AppUserInvite row (the email is sent by the host). Mutating users from
|
|
1755
1754
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
1756
1755
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
1757
1756
|
// that only call read methods need only `users.read:*`.
|
|
@@ -1769,13 +1768,8 @@ const HOOKS = [
|
|
|
1769
1768
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1770
1769
|
"sendPasswordReset) need users.write:*, and the destructive remove() " +
|
|
1771
1770
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
1772
|
-
"{ email, name, group_ids
|
|
1773
|
-
"
|
|
1774
|
-
"The host emails the invitation at once unless send_invitation is " +
|
|
1775
|
-
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
1776
|
-
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
1777
|
-
"later. A send_invitation that is not a boolean rejects with " +
|
|
1778
|
-
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
1771
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
1772
|
+
"(the email is sent by the host). sendPasswordReset(userId) mails the " +
|
|
1779
1773
|
"standard self-serve reset link to that user's own registered address " +
|
|
1780
1774
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
1781
1775
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -1788,7 +1782,7 @@ const HOOKS = [
|
|
|
1788
1782
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1789
1783
|
refetch: "() => Promise<void>",
|
|
1790
1784
|
invite:
|
|
1791
|
-
"({ email, name, group_ids
|
|
1785
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
1792
1786
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1793
1787
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1794
1788
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -1833,8 +1827,7 @@ const HOOKS = [
|
|
|
1833
1827
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
1834
1828
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1835
1829
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1836
|
-
// caller's `users.write` SystemAcl capability.
|
|
1837
|
-
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
1830
|
+
// caller's `users.write` SystemAcl capability.
|
|
1838
1831
|
{
|
|
1839
1832
|
name: "useInvites",
|
|
1840
1833
|
signature: "useInvites(query)",
|
|
@@ -1844,23 +1837,13 @@ const HOOKS = [
|
|
|
1844
1837
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1845
1838
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1846
1839
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1847
|
-
"res.data; rows are snake_case (email, status,
|
|
1848
|
-
"
|
|
1849
|
-
"
|
|
1850
|
-
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
1851
|
-
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
1852
|
-
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
1853
|
-
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
1854
|
-
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
1855
|
-
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
1856
|
-
"'pending'. There is no separate send method; label the button " +
|
|
1857
|
-
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
1858
|
-
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1840
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1841
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1842
|
+
"value. query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1859
1843
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
1860
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
1861
|
-
"
|
|
1862
|
-
"
|
|
1863
|
-
"not_sent or expired). q is a " +
|
|
1844
|
+
"{ status: 'pending' } for the usual outstanding-invite list; " +
|
|
1845
|
+
"status 'open' is every invite neither accepted nor revoked (pending " +
|
|
1846
|
+
"or expired). q is a " +
|
|
1864
1847
|
"case-insensitive substring match on the invite's name or email. " +
|
|
1865
1848
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
1866
1849
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -1877,12 +1860,12 @@ const HOOKS = [
|
|
|
1877
1860
|
"refetch on success.",
|
|
1878
1861
|
returnShape: {
|
|
1879
1862
|
invites:
|
|
1880
|
-
"Array<{ id, app_user_id, email, name, group_ids, status,
|
|
1863
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1881
1864
|
loading: "boolean",
|
|
1882
1865
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1883
1866
|
refetch: "() => Promise<void>",
|
|
1884
1867
|
resend:
|
|
1885
|
-
"(inviteId) => Promise<Invite> //
|
|
1868
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1886
1869
|
revoke:
|
|
1887
1870
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1888
1871
|
},
|
|
@@ -2060,6 +2043,13 @@ const HOOKS = [
|
|
|
2060
2043
|
"table governed by per-record grants, since a scope-less subscribe must " +
|
|
2061
2044
|
"prove read-every-row. A `parent` scope requires the column be an " +
|
|
2062
2045
|
"inheritAcl RELATION, else the subscribe resolves to " +
|
|
2046
|
+
"{ status: 'fallback' }. { kind: 'granted' } streams every row the " +
|
|
2047
|
+
"signed-in user can read through a grant to them or to one of their " +
|
|
2048
|
+
"groups — the way to keep a list of 'my conversations' or 'my cases' " +
|
|
2049
|
+
"live — and adds two handlers: onAccessGained(record) when a row " +
|
|
2050
|
+
"becomes readable (the full row) and onAccessLost({ id }) when it stops " +
|
|
2051
|
+
"being readable, after which no further event for it arrives. A " +
|
|
2052
|
+
"logged-out visitor's granted subscribe resolves to " +
|
|
2063
2053
|
"{ status: 'fallback' }.",
|
|
2064
2054
|
returnShape: {
|
|
2065
2055
|
status:
|
|
@@ -3038,7 +3028,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3038
3028
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
3039
3029
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
3040
3030
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
3041
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites
|
|
3031
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke pending invites, gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:* (destructive user removal by users.delete:*).",
|
|
3042
3032
|
required: true,
|
|
3043
3033
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
3044
3034
|
},
|
|
@@ -5066,7 +5056,14 @@ const CONTRACT = deepFreeze({
|
|
|
5066
5056
|
// `useAudioPlayer(uri)`, backed by the optional `device.audioRecorder` /
|
|
5067
5057
|
// `device.audioPlayer` slices (MediaRecorder + <audio> on web, expo-audio
|
|
5068
5058
|
// on native), with `AudioRecorderError` / `AudioPlayerError`.
|
|
5069
|
-
|
|
5059
|
+
// 1.154.0: additive (sc-10589) — `useDatastoreSubscription` accepts
|
|
5060
|
+
// `scope: { kind: "granted" }` (the rows the signed-in user reads through
|
|
5061
|
+
// a user or group grant) and the `onAccessGained` / `onAccessLost`
|
|
5062
|
+
// handlers that report rows entering and leaving that set.
|
|
5063
|
+
// 1.155.0: narrowing (sc-10566) — withdraws 1.144.0: `useUsers().invite`
|
|
5064
|
+
// takes no `send_invitation` and always emails, invite rows carry no
|
|
5065
|
+
// `sent_at`, and `not_sent` is no longer a status.
|
|
5066
|
+
version: "1.155.0",
|
|
5070
5067
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5071
5068
|
hooks: HOOKS,
|
|
5072
5069
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -3979,10 +3979,13 @@ export function useCanWrite(tableId, options) {
|
|
|
3979
3979
|
* rather than crashing at render (widget-parity skill).
|
|
3980
3980
|
*
|
|
3981
3981
|
* @param {string} table Bound table id (falsy → no subscription, status "fallback").
|
|
3982
|
-
* @param {{ onCreated?, onUpdated?, onDeleted? }} [handlers] Per-event callbacks; each receives the snake_case record.
|
|
3983
|
-
* @param {{ fallbackAfterMs?: number, scope?: { kind: "record"|"parent", relation_column?: string, record_id
|
|
3982
|
+
* @param {{ onCreated?, onUpdated?, onDeleted?, onAccessGained?, onAccessLost? }} [handlers] Per-event callbacks; each receives the snake_case record.
|
|
3983
|
+
* @param {{ fallbackAfterMs?: number, scope?: { kind: "record"|"parent"|"granted", relation_column?: string, record_id?: string } }} [options]
|
|
3984
3984
|
* `scope` narrows the stream below whole-table — required for a table
|
|
3985
3985
|
* governed by per-record grants. Re-subscribes on its VALUES, not identity.
|
|
3986
|
+
* `{ kind: "granted" }` streams every row the signed-in user reads through
|
|
3987
|
+
* a user or group grant; `onAccessGained` / `onAccessLost` report rows
|
|
3988
|
+
* entering (the full row) and leaving (`{ id }`) that set.
|
|
3986
3989
|
* @returns {{ status: "connecting" | "live" | "reconnecting" | "fallback" }}
|
|
3987
3990
|
*/
|
|
3988
3991
|
export function useDatastoreSubscription(table, handlers, options) {
|
|
@@ -4051,6 +4054,8 @@ export function useDatastoreSubscription(table, handlers, options) {
|
|
|
4051
4054
|
onCreated: (r) => handlersRef.current?.onCreated?.(r),
|
|
4052
4055
|
onUpdated: (r) => handlersRef.current?.onUpdated?.(r),
|
|
4053
4056
|
onDeleted: (r) => handlersRef.current?.onDeleted?.(r),
|
|
4057
|
+
onAccessGained: (r) => handlersRef.current?.onAccessGained?.(r),
|
|
4058
|
+
onAccessLost: (r) => handlersRef.current?.onAccessLost?.(r),
|
|
4054
4059
|
onStatus: (s) => setStatus(s),
|
|
4055
4060
|
},
|
|
4056
4061
|
Object.keys(subscribeOptions).length > 0 ? subscribeOptions : undefined,
|
|
@@ -5534,7 +5539,7 @@ export function useDirectory(query) {
|
|
|
5534
5539
|
* invite, deactivate, reactivate}` — `list` resolves to the `{ data, meta }`
|
|
5535
5540
|
* envelope VERBATIM, so we unwrap `res.data` (default `[]`). User rows are
|
|
5536
5541
|
* snake_case (`id`, `name`, `email`, `role`, `is_active`, …) and bodies
|
|
5537
|
-
* (e.g. `{ email, name, group_ids
|
|
5542
|
+
* (e.g. `{ email, name, group_ids? }`) pass through verbatim. `query` is an
|
|
5538
5543
|
* optional `{ q?, role?, is_active?, group_id?, limit?, offset? }`; `group_id`
|
|
5539
5544
|
* (the `id` of a `useGroups()` row) narrows the list to that group's members.
|
|
5540
5545
|
* The list refetches whenever `JSON.stringify(query)` changes; the imperative methods
|
|
@@ -5771,20 +5776,15 @@ export function useGroups(query) {
|
|
|
5771
5776
|
* `{ data, meta }` envelope VERBATIM, so we unwrap `res.data` (default `[]`).
|
|
5772
5777
|
*
|
|
5773
5778
|
* Rows are snake_case exactly as the backend projects them: `{ id, tenant_id,
|
|
5774
|
-
* email, name, group_ids, status,
|
|
5775
|
-
*
|
|
5776
|
-
*
|
|
5777
|
-
*
|
|
5778
|
-
* whose clock may disagree.
|
|
5779
|
-
*
|
|
5780
|
-
* `not_sent` is an invite created with `send_invitation: false`: `sent_at` is
|
|
5781
|
-
* null and nothing was emailed. `resend(inviteId)` is the one call that sends
|
|
5782
|
-
* — a first send and a repeat send alike — and resolves the row as `pending`.
|
|
5779
|
+
* email, name, group_ids, status, expires_at, accepted_at, revoked_at,
|
|
5780
|
+
* created_at, invited_by_studio_user_id }`. `status` is the server-computed
|
|
5781
|
+
* lifecycle value (`"pending" | "accepted" | "revoked" | "expired"`) — prefer
|
|
5782
|
+
* it over re-deriving expiry on the client, whose clock may disagree.
|
|
5783
5783
|
*
|
|
5784
5784
|
* `query` is an optional `{ status?, limit?, offset? }` passed through
|
|
5785
5785
|
* verbatim; `status` accepts `"pending" | "accepted" | "revoked" | "expired" |
|
|
5786
|
-
* "
|
|
5787
|
-
*
|
|
5786
|
+
* "all"` and the endpoint defaults to `"all"`, so pass `{ status: "pending" }`
|
|
5787
|
+
* for the usual "who hasn't accepted yet" list.
|
|
5788
5788
|
*
|
|
5789
5789
|
* The WHOLE invite surface — list included — is gated on the `users.write`
|
|
5790
5790
|
* capability plus a signed `users.write:*` scope: a pending invite exposes
|
|
@@ -5851,8 +5851,7 @@ export function useInvites(query) {
|
|
|
5851
5851
|
}, [doFetch]);
|
|
5852
5852
|
|
|
5853
5853
|
// Both mutations re-list on success: revoking drops the row and resending
|
|
5854
|
-
// moves `expires_at
|
|
5855
|
-
// show the operator the old state.
|
|
5854
|
+
// moves `expires_at`, so a stale list would show the operator the old state.
|
|
5856
5855
|
const resend = useCallback(
|
|
5857
5856
|
async (inviteId) => {
|
|
5858
5857
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -1418,6 +1418,10 @@ export interface DatastoreSubscriptionHandlers {
|
|
|
1418
1418
|
onCreated?: (record: Record<string, unknown>) => void;
|
|
1419
1419
|
onUpdated?: (record: Record<string, unknown>) => void;
|
|
1420
1420
|
onDeleted?: (record: Record<string, unknown>) => void;
|
|
1421
|
+
/** Granted scope: a row the user can now read, in full. */
|
|
1422
|
+
onAccessGained?: (record: Record<string, unknown>) => void;
|
|
1423
|
+
/** Granted scope: a row the user can no longer read; nothing follows it. */
|
|
1424
|
+
onAccessLost?: (record: { id: string }) => void;
|
|
1421
1425
|
}
|
|
1422
1426
|
|
|
1423
1427
|
// sc-6270: see SubscriptionScope in @colixsystems/datastore-client. A scoped
|
|
@@ -1425,7 +1429,8 @@ export interface DatastoreSubscriptionHandlers {
|
|
|
1425
1429
|
// which is what lets a per-record-ACL table stream at all.
|
|
1426
1430
|
export type DatastoreSubscriptionScope =
|
|
1427
1431
|
| { kind: "record"; record_id: string }
|
|
1428
|
-
| { kind: "parent"; relation_column: string; record_id: string }
|
|
1432
|
+
| { kind: "parent"; relation_column: string; record_id: string }
|
|
1433
|
+
| { kind: "granted" };
|
|
1429
1434
|
|
|
1430
1435
|
export interface DatastoreSubscriptionOptions {
|
|
1431
1436
|
fallbackAfterMs?: number;
|
|
@@ -1440,9 +1445,10 @@ export interface DatastoreSubscriptionOptions {
|
|
|
1440
1445
|
* REST polling instead. Never throws — degrades to `{ status: "fallback" }` on
|
|
1441
1446
|
* a host whose datastore client predates realtime.
|
|
1442
1447
|
*
|
|
1443
|
-
* `options.scope` narrows the stream to one record
|
|
1444
|
-
* inheriting children
|
|
1445
|
-
*
|
|
1448
|
+
* `options.scope` narrows the stream to one record, to a parent's
|
|
1449
|
+
* inheriting children, or (`{ kind: "granted" }`) to the rows the signed-in
|
|
1450
|
+
* user reads through a user or group grant; re-subscribes when the scope's
|
|
1451
|
+
* values change, not on every render.
|
|
1446
1452
|
*/
|
|
1447
1453
|
export function useDatastoreSubscription(
|
|
1448
1454
|
tableId: string | null | undefined,
|
|
@@ -2436,25 +2442,13 @@ export interface InviteArgs {
|
|
|
2436
2442
|
email: string;
|
|
2437
2443
|
name?: string;
|
|
2438
2444
|
group_ids?: string[];
|
|
2439
|
-
/**
|
|
2440
|
-
* Defaults to `true`: the invitation is emailed at once. `false` stores it
|
|
2441
|
-
* without emailing anyone (`status: "not_sent"`); send it later with
|
|
2442
|
-
* `useInvites().resend(inviteId)`.
|
|
2443
|
-
*/
|
|
2444
|
-
send_invitation?: boolean;
|
|
2445
2445
|
}
|
|
2446
2446
|
|
|
2447
|
-
/**
|
|
2448
|
-
* When several apply the first of accepted, revoked, not_sent, expired,
|
|
2449
|
-
* pending wins. `not_sent` is an invitation created with
|
|
2450
|
-
* `send_invitation: false` that has not been emailed yet.
|
|
2451
|
-
*/
|
|
2452
2447
|
export type AppUserInviteStatus =
|
|
2453
2448
|
| "pending"
|
|
2454
2449
|
| "accepted"
|
|
2455
2450
|
| "revoked"
|
|
2456
|
-
| "expired"
|
|
2457
|
-
| "not_sent";
|
|
2451
|
+
| "expired";
|
|
2458
2452
|
|
|
2459
2453
|
export interface AppUserInviteRow {
|
|
2460
2454
|
id: string;
|
|
@@ -2469,9 +2463,7 @@ export interface AppUserInviteRow {
|
|
|
2469
2463
|
status: AppUserInviteStatus;
|
|
2470
2464
|
name?: string;
|
|
2471
2465
|
group_ids?: string[];
|
|
2472
|
-
|
|
2473
|
-
sent_at: string | null;
|
|
2474
|
-
/** Meaningless while `status` is `"not_sent"` — it restarts on the send. */
|
|
2466
|
+
invited_at?: string;
|
|
2475
2467
|
expires_at?: string;
|
|
2476
2468
|
accepted_at?: string | null;
|
|
2477
2469
|
revoked_at?: string | null;
|
|
@@ -2540,9 +2532,8 @@ export function useGroups(query?: GroupsQuery): GroupsApi;
|
|
|
2540
2532
|
|
|
2541
2533
|
export interface InvitesQuery {
|
|
2542
2534
|
/**
|
|
2543
|
-
* Defaults to "all" server-side; pass "pending" for outstanding invites
|
|
2544
|
-
* "
|
|
2545
|
-
* "open" is "pending", "not_sent" or "expired": neither accepted nor revoked.
|
|
2535
|
+
* Defaults to "all" server-side; pass "pending" for outstanding invites.
|
|
2536
|
+
* "open" is "pending" or "expired": neither accepted nor revoked.
|
|
2546
2537
|
*/
|
|
2547
2538
|
status?: AppUserInviteStatus | "open" | "all";
|
|
2548
2539
|
/** Case-insensitive substring match on the invite's name or email. */
|
|
@@ -2563,12 +2554,7 @@ export interface InvitesApi {
|
|
|
2563
2554
|
loading: boolean;
|
|
2564
2555
|
error: DirectoryError | null;
|
|
2565
2556
|
refetch(): Promise<void>;
|
|
2566
|
-
/**
|
|
2567
|
-
* Email the invitation and restart its 7-day expiry. The one call that
|
|
2568
|
-
* sends: the first send of a `not_sent` invitation and a repeat send of a
|
|
2569
|
-
* sent one alike. Resolves the row with `status: "pending"`; refetches on
|
|
2570
|
-
* success.
|
|
2571
|
-
*/
|
|
2557
|
+
/** Re-send the invitation email. Refetches on success. */
|
|
2572
2558
|
resend(inviteId: string): Promise<AppUserInviteRow>;
|
|
2573
2559
|
/** Cancel a pending invitation. Refetches on success. */
|
|
2574
2560
|
revoke(inviteId: string): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.194.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -1749,9 +1749,8 @@ const HOOKS = [
|
|
|
1749
1749
|
// deactivate / reactivate / sendPasswordReset) additionally need
|
|
1750
1750
|
// `users.write:*`, and the
|
|
1751
1751
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
1752
|
-
// call accepts `{ email, name,
|
|
1753
|
-
//
|
|
1754
|
-
// `send_invitation` is false). Mutating users from
|
|
1752
|
+
// call accepts `{ email, name, groupIds? }` and returns the resulting
|
|
1753
|
+
// AppUserInvite row (the email is sent by the host). Mutating users from
|
|
1755
1754
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
1756
1755
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
1757
1756
|
// that only call read methods need only `users.read:*`.
|
|
@@ -1769,13 +1768,8 @@ const HOOKS = [
|
|
|
1769
1768
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
1770
1769
|
"sendPasswordReset) need users.write:*, and the destructive remove() " +
|
|
1771
1770
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
1772
|
-
"{ email, name, group_ids
|
|
1773
|
-
"
|
|
1774
|
-
"The host emails the invitation at once unless send_invitation is " +
|
|
1775
|
-
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
1776
|
-
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
1777
|
-
"later. A send_invitation that is not a boolean rejects with " +
|
|
1778
|
-
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
1771
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
1772
|
+
"(the email is sent by the host). sendPasswordReset(userId) mails the " +
|
|
1779
1773
|
"standard self-serve reset link to that user's own registered address " +
|
|
1780
1774
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
1781
1775
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -1788,7 +1782,7 @@ const HOOKS = [
|
|
|
1788
1782
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1789
1783
|
refetch: "() => Promise<void>",
|
|
1790
1784
|
invite:
|
|
1791
|
-
"({ email, name, group_ids
|
|
1785
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
1792
1786
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1793
1787
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
1794
1788
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -1833,8 +1827,7 @@ const HOOKS = [
|
|
|
1833
1827
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
1834
1828
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
1835
1829
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
1836
|
-
// caller's `users.write` SystemAcl capability.
|
|
1837
|
-
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
1830
|
+
// caller's `users.write` SystemAcl capability.
|
|
1838
1831
|
{
|
|
1839
1832
|
name: "useInvites",
|
|
1840
1833
|
signature: "useInvites(query)",
|
|
@@ -1844,23 +1837,13 @@ const HOOKS = [
|
|
|
1844
1837
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
1845
1838
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
1846
1839
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
1847
|
-
"res.data; rows are snake_case (email, status,
|
|
1848
|
-
"
|
|
1849
|
-
"
|
|
1850
|
-
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
1851
|
-
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
1852
|
-
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
1853
|
-
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
1854
|
-
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
1855
|
-
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
1856
|
-
"'pending'. There is no separate send method; label the button " +
|
|
1857
|
-
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
1858
|
-
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1840
|
+
"res.data; rows are snake_case (email, status, expires_at, …) and " +
|
|
1841
|
+
"status is the server-computed pending/accepted/revoked/expired " +
|
|
1842
|
+
"value. query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
1859
1843
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
1860
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
1861
|
-
"
|
|
1862
|
-
"
|
|
1863
|
-
"not_sent or expired). q is a " +
|
|
1844
|
+
"{ status: 'pending' } for the usual outstanding-invite list; " +
|
|
1845
|
+
"status 'open' is every invite neither accepted nor revoked (pending " +
|
|
1846
|
+
"or expired). q is a " +
|
|
1864
1847
|
"case-insensitive substring match on the invite's name or email. " +
|
|
1865
1848
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
1866
1849
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -1877,12 +1860,12 @@ const HOOKS = [
|
|
|
1877
1860
|
"refetch on success.",
|
|
1878
1861
|
returnShape: {
|
|
1879
1862
|
invites:
|
|
1880
|
-
"Array<{ id, app_user_id, email, name, group_ids, status,
|
|
1863
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; unwrapped from { data, meta }",
|
|
1881
1864
|
loading: "boolean",
|
|
1882
1865
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
1883
1866
|
refetch: "() => Promise<void>",
|
|
1884
1867
|
resend:
|
|
1885
|
-
"(inviteId) => Promise<Invite> //
|
|
1868
|
+
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
1886
1869
|
revoke:
|
|
1887
1870
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
1888
1871
|
},
|
|
@@ -2060,6 +2043,13 @@ const HOOKS = [
|
|
|
2060
2043
|
"table governed by per-record grants, since a scope-less subscribe must " +
|
|
2061
2044
|
"prove read-every-row. A `parent` scope requires the column be an " +
|
|
2062
2045
|
"inheritAcl RELATION, else the subscribe resolves to " +
|
|
2046
|
+
"{ status: 'fallback' }. { kind: 'granted' } streams every row the " +
|
|
2047
|
+
"signed-in user can read through a grant to them or to one of their " +
|
|
2048
|
+
"groups — the way to keep a list of 'my conversations' or 'my cases' " +
|
|
2049
|
+
"live — and adds two handlers: onAccessGained(record) when a row " +
|
|
2050
|
+
"becomes readable (the full row) and onAccessLost({ id }) when it stops " +
|
|
2051
|
+
"being readable, after which no further event for it arrives. A " +
|
|
2052
|
+
"logged-out visitor's granted subscribe resolves to " +
|
|
2063
2053
|
"{ status: 'fallback' }.",
|
|
2064
2054
|
returnShape: {
|
|
2065
2055
|
status:
|
|
@@ -3038,7 +3028,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3038
3028
|
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
3039
3029
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
3040
3030
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() } }. " +
|
|
3041
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites
|
|
3031
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke pending invites, gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:* (destructive user removal by users.delete:*).",
|
|
3042
3032
|
required: true,
|
|
3043
3033
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object" },
|
|
3044
3034
|
},
|
|
@@ -5066,7 +5056,14 @@ const CONTRACT = deepFreeze({
|
|
|
5066
5056
|
// `useAudioPlayer(uri)`, backed by the optional `device.audioRecorder` /
|
|
5067
5057
|
// `device.audioPlayer` slices (MediaRecorder + <audio> on web, expo-audio
|
|
5068
5058
|
// on native), with `AudioRecorderError` / `AudioPlayerError`.
|
|
5069
|
-
|
|
5059
|
+
// 1.154.0: additive (sc-10589) — `useDatastoreSubscription` accepts
|
|
5060
|
+
// `scope: { kind: "granted" }` (the rows the signed-in user reads through
|
|
5061
|
+
// a user or group grant) and the `onAccessGained` / `onAccessLost`
|
|
5062
|
+
// handlers that report rows entering and leaving that set.
|
|
5063
|
+
// 1.155.0: narrowing (sc-10566) — withdraws 1.144.0: `useUsers().invite`
|
|
5064
|
+
// takes no `send_invitation` and always emails, invite rows carry no
|
|
5065
|
+
// `sent_at`, and `not_sent` is no longer a status.
|
|
5066
|
+
version: "1.155.0",
|
|
5070
5067
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5071
5068
|
hooks: HOOKS,
|
|
5072
5069
|
primitives: PRIMITIVES,
|