@colixsystems/widget-sdk 0.265.0 → 0.266.1
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 +45 -9
- package/dist/contract.cjs +37 -16
- package/dist/contract.js +37 -16
- package/dist/hooks.js +14 -8
- package/dist/index.d.ts +25 -5
- package/dist/property-schema.js +3 -0
- package/dist/web-modal.js +28 -0
- package/package.json +1 -1
- package/src/contract.js +37 -16
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
69
69
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query)` (optional: query) | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
70
70
|
| **DIRECTORY** | `useUsers(query)` (optional: query) | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove, sendPasswordReset, update }` | `directory.users.*` — `users.read:*` plus the `users.read` (or `users.write`) capability, else `FORBIDDEN` (edits, incl. `sendPasswordReset()` and `update()`, also `users.write:*`; `remove()` also `users.delete:*`) |
|
|
71
71
|
| **DIRECTORY** | `useGroups(query)` (optional: query) | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
72
|
-
| **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. |
|
|
72
|
+
| **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 \| not_sent \| open \| all` (endpoint default `all`; `open` = `pending`, `not_sent` or `expired`); `q` matches the invite's name or email, `app_user_id` returns the invites reserving that user id. `resend(inviteId)` sends the invitation — the first send of a `not_sent` one included. |
|
|
73
73
|
| **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
|
|
74
74
|
| **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. |
|
|
75
75
|
| **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). |
|
|
@@ -86,7 +86,32 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
86
86
|
|
|
87
87
|
## Status
|
|
88
88
|
|
|
89
|
-
`v0.
|
|
89
|
+
`v0.266.1` — 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**.
|
|
90
|
+
|
|
91
|
+
### What's new in 0.266.1 (contract 1.216.0)
|
|
92
|
+
|
|
93
|
+
**An open `<Modal>` or `<Overlay>` holds the page still on the web.** Scrolling past the end of a dialog's body, or over a dialog with nothing to scroll, used to scroll the page behind it. The page now stays put until the dialog closes, as it already did in the phone app. Nothing changes for a widget. An anchored `<Menu>`, `<Popover>` or `<Tooltip>` still closes when the page scrolls under it.
|
|
94
|
+
|
|
95
|
+
### What's new in 0.266.0 (contract 1.216.0)
|
|
96
|
+
|
|
97
|
+
**A widget can prepare an invitation now and send it later, again.** 0.194.0 withdrew this; it is back with the same shape. Without it `useUsers().invite()` emails the invitation the moment it is created, so a widget could not line people up ahead of a launch day or let a second person check the list first. `invite()` now takes an optional `send_invitation`:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
const { invite } = useUsers();
|
|
101
|
+
const { invites, resend } = useInvites({ status: "not_sent" });
|
|
102
|
+
|
|
103
|
+
const prepared = await invite({ email: "bo@example.com", name: "Bo", send_invitation: false });
|
|
104
|
+
// prepared.status === "not_sent", prepared.sent_at === null — nobody was emailed.
|
|
105
|
+
|
|
106
|
+
await resend(prepared.id); // emails it; the row is now "pending" with a fresh 7-day expiry
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- **`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`.
|
|
110
|
+
- **`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.
|
|
111
|
+
- **`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.
|
|
112
|
+
- **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.
|
|
113
|
+
|
|
114
|
+
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.
|
|
90
115
|
|
|
91
116
|
### What's new in 0.265.0 (contract 1.215.0)
|
|
92
117
|
|
|
@@ -1311,7 +1336,7 @@ An installed widget is not rebuilt or republished for this. The web Player hands
|
|
|
1311
1336
|
|
|
1312
1337
|
### What's new in 0.183.0 (contract 1.144.0)
|
|
1313
1338
|
|
|
1314
|
-
Send-later invitations (`send_invitation`, the `not_sent` status, `sent_at`). Withdrawn in 0.194.0.
|
|
1339
|
+
Send-later invitations (`send_invitation`, the `not_sent` status, `sent_at`). Withdrawn in 0.194.0, reinstated in 0.266.0.
|
|
1315
1340
|
|
|
1316
1341
|
### What's new in 0.182.0 (contract 1.143.0)
|
|
1317
1342
|
|
|
@@ -3349,11 +3374,13 @@ The matching manifest declares the scopes:
|
|
|
3349
3374
|
}
|
|
3350
3375
|
```
|
|
3351
3376
|
|
|
3377
|
+
`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)`.
|
|
3378
|
+
|
|
3352
3379
|
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.
|
|
3353
3380
|
|
|
3354
3381
|
## Listing pending invitations from a widget
|
|
3355
3382
|
|
|
3356
|
-
`useInvites(query?)` lists the workspace's invites and
|
|
3383
|
+
`useInvites(query?)` lists the workspace's invites and sends / revokes them:
|
|
3357
3384
|
|
|
3358
3385
|
```jsx
|
|
3359
3386
|
import { useInvites, View, Text, Pressable } from '@colixsystems/widget-sdk';
|
|
@@ -3368,7 +3395,9 @@ function PendingInvites() {
|
|
|
3368
3395
|
{invites.map((i) => (
|
|
3369
3396
|
<View key={i.id}>
|
|
3370
3397
|
<Text>{i.email} · {i.status}</Text>
|
|
3371
|
-
<Pressable onPress={() => resend(i.id).catch(() => {})}
|
|
3398
|
+
<Pressable onPress={() => resend(i.id).catch(() => {})}>
|
|
3399
|
+
<Text>{i.status === 'not_sent' ? 'Send invitation' : 'Resend'}</Text>
|
|
3400
|
+
</Pressable>
|
|
3372
3401
|
<Pressable onPress={() => revoke(i.id).catch(() => {})}><Text>Revoke</Text></Pressable>
|
|
3373
3402
|
</View>
|
|
3374
3403
|
))}
|
|
@@ -3377,9 +3406,16 @@ function PendingInvites() {
|
|
|
3377
3406
|
}
|
|
3378
3407
|
```
|
|
3379
3408
|
|
|
3380
|
-
Rows are snake_case: `{ id, app_user_id, email, name, group_ids, status,
|
|
3381
|
-
accepted_at, revoked_at, created_at }`.
|
|
3382
|
-
|
|
3409
|
+
Rows are snake_case: `{ id, app_user_id, email, name, group_ids, status, sent_at,
|
|
3410
|
+
expires_at, accepted_at, revoked_at, created_at }`. `status` is one of `pending`,
|
|
3411
|
+
`accepted`, `revoked`, `expired` or `not_sent`; trust the server-computed value rather
|
|
3412
|
+
than comparing `expires_at` against the device clock.
|
|
3413
|
+
|
|
3414
|
+
A `not_sent` row is an invitation created with `send_invitation: false`: nobody has
|
|
3415
|
+
been emailed and `sent_at` is `null`. `resend(inviteId)` is the one call that sends
|
|
3416
|
+
an invitation, a first send and a repeat send alike — it emails the invitee, stamps
|
|
3417
|
+
`sent_at`, restarts the 7-day expiry and resolves the row as `pending`. Pass
|
|
3418
|
+
`{ status: 'not_sent' }` to list only the prepared ones.
|
|
3383
3419
|
|
|
3384
3420
|
`query` also takes `q` (a case-insensitive substring match on the invite's name or
|
|
3385
3421
|
email) and `app_user_id`. Each row's `app_user_id` is the id the invitee's user account
|
|
@@ -3387,7 +3423,7 @@ will have once they accept. A `USER` column may hold that id before the person h
|
|
|
3387
3423
|
joined — it then reads back as `{ id, name, invited: true }`, with no `email` — and
|
|
3388
3424
|
`useInvites({ app_user_id: value.id, status: 'open', limit: 1 })` returns the invite
|
|
3389
3425
|
behind it. `status: 'open'` is every invite that is neither accepted nor revoked
|
|
3390
|
-
(`pending` or `expired`); a re-invite keeps the id, so without it earlier invites match too.
|
|
3426
|
+
(`pending`, `not_sent` or `expired`); a re-invite keeps the id, so without it earlier invites match too.
|
|
3391
3427
|
|
|
3392
3428
|
**The gate is `users.write`, not `users.read`.** A pending invite exposes the email of
|
|
3393
3429
|
someone who is not a member yet, so the backend gates the entire invite surface —
|
package/dist/contract.cjs
CHANGED
|
@@ -2361,8 +2361,9 @@ const HOOKS = [
|
|
|
2361
2361
|
// deactivate / reactivate / sendPasswordReset / update) additionally need
|
|
2362
2362
|
// `users.write:*`, and the
|
|
2363
2363
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
2364
|
-
// call accepts `{ email, name,
|
|
2365
|
-
// AppUserInvite row (the
|
|
2364
|
+
// call accepts `{ email, name, group_ids?, send_invitation? }` and returns
|
|
2365
|
+
// the resulting AppUserInvite row (the host sends the email, unless
|
|
2366
|
+
// `send_invitation` is false). Mutating users from
|
|
2366
2367
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
2367
2368
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
2368
2369
|
// that only call read methods need only `users.read:*`.
|
|
@@ -2382,8 +2383,13 @@ const HOOKS = [
|
|
|
2382
2383
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
2383
2384
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
2384
2385
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
2385
|
-
"{ email, name, group_ids? } and returns the resulting
|
|
2386
|
-
"
|
|
2386
|
+
"{ email, name, group_ids?, send_invitation? } and returns the resulting " +
|
|
2387
|
+
"AppUserInvite row { id, email, name, expires_at, sent_at, status }. " +
|
|
2388
|
+
"The host emails the invitation at once unless send_invitation is " +
|
|
2389
|
+
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
2390
|
+
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
2391
|
+
"later. A send_invitation that is not a boolean rejects with " +
|
|
2392
|
+
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
2387
2393
|
"standard self-serve reset link to that user's own registered address " +
|
|
2388
2394
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
2389
2395
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -2422,7 +2428,7 @@ const HOOKS = [
|
|
|
2422
2428
|
error: "DirectoryError | null // { code, reason, message, retryable }",
|
|
2423
2429
|
refetch: "() => Promise<void>",
|
|
2424
2430
|
invite:
|
|
2425
|
-
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
2431
|
+
"({ email, name, group_ids?, send_invitation? }) => Promise<Invite> // send_invitation: false stores it unsent (status 'not_sent'); rejects with DirectoryError",
|
|
2426
2432
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2427
2433
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2428
2434
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -2640,7 +2646,8 @@ const HOOKS = [
|
|
|
2640
2646
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
2641
2647
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
2642
2648
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
2643
|
-
// caller's `users.write` SystemAcl capability.
|
|
2649
|
+
// caller's `users.write` SystemAcl capability. sc-9714: `resend` is also
|
|
2650
|
+
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
2644
2651
|
{
|
|
2645
2652
|
name: "useInvites",
|
|
2646
2653
|
signature: "useInvites(query)",
|
|
@@ -2650,13 +2657,23 @@ const HOOKS = [
|
|
|
2650
2657
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
2651
2658
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
2652
2659
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
2653
|
-
"res.data; rows are snake_case (email, status, expires_at, …)
|
|
2654
|
-
"status is the server-computed pending/accepted/revoked/expired
|
|
2655
|
-
"value.
|
|
2660
|
+
"res.data; rows are snake_case (email, status, sent_at, expires_at, …) " +
|
|
2661
|
+
"and status is the server-computed pending/accepted/revoked/expired/" +
|
|
2662
|
+
"not_sent value. not_sent is an invitation created with " +
|
|
2663
|
+
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
2664
|
+
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
2665
|
+
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
2666
|
+
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
2667
|
+
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
2668
|
+
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
2669
|
+
"'pending'. There is no separate send method; label the button " +
|
|
2670
|
+
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
2671
|
+
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
2656
2672
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
2657
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
2658
|
-
"status '
|
|
2659
|
-
"
|
|
2673
|
+
"{ status: 'pending' } for the usual outstanding-invite list and " +
|
|
2674
|
+
"{ status: 'not_sent' } for the prepared ones; " +
|
|
2675
|
+
"status 'open' is every invite neither accepted nor revoked (pending, " +
|
|
2676
|
+
"not_sent or expired). q is a " +
|
|
2660
2677
|
"case-insensitive substring match on the invite's name or email. " +
|
|
2661
2678
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
2662
2679
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -2673,12 +2690,12 @@ const HOOKS = [
|
|
|
2673
2690
|
"refetch on success.",
|
|
2674
2691
|
returnShape: {
|
|
2675
2692
|
invites:
|
|
2676
|
-
"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 }",
|
|
2693
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, sent_at, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; status is pending | accepted | revoked | expired | not_sent; unwrapped from { data, meta }",
|
|
2677
2694
|
loading: "boolean",
|
|
2678
2695
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
2679
2696
|
refetch: "() => Promise<void>",
|
|
2680
2697
|
resend:
|
|
2681
|
-
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
2698
|
+
"(inviteId) => Promise<Invite> // sends it (first send of a not_sent invite too); refetches; rejects with DirectoryError",
|
|
2682
2699
|
revoke:
|
|
2683
2700
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
2684
2701
|
},
|
|
@@ -4172,7 +4189,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4172
4189
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
4173
4190
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() }, " +
|
|
4174
4191
|
"presence: { online(tableId, recordIds) -> { data: [{ record_id, online_user_ids, online_count }] }, me() -> { show_online }, setShowOnline(bool) -> { show_online } } }. " +
|
|
4175
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke
|
|
4192
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites — resend also sends one created with send_invitation: false — gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope); presence backs useOnlineMembers() + useOnlineVisibility() (session-gated, no scope header). 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:*).",
|
|
4176
4193
|
required: true,
|
|
4177
4194
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object", presence: "object" },
|
|
4178
4195
|
},
|
|
@@ -6445,7 +6462,11 @@ const CONTRACT = deepFreeze({
|
|
|
6445
6462
|
// random id per row and may be `defaultLocked`.
|
|
6446
6463
|
// 1.215.0: additive (sc-13938) — `usePdfExport` `openPdf` / `exportToPdf`
|
|
6447
6464
|
// take `appendFileIds`: stored PDFs whose pages follow the rendered HTML.
|
|
6448
|
-
|
|
6465
|
+
// 1.216.0: additive (sc-14096) — reinstates 1.144.0, withdrawn in 1.155.0:
|
|
6466
|
+
// `useUsers().invite` takes `send_invitation` (false stores the invite
|
|
6467
|
+
// unsent), invite rows carry `sent_at`, `not_sent` is a status, and
|
|
6468
|
+
// `useInvites().resend` performs the first send.
|
|
6469
|
+
version: "1.216.0",
|
|
6449
6470
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6450
6471
|
hooks: HOOKS,
|
|
6451
6472
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -2358,8 +2358,9 @@ const HOOKS = [
|
|
|
2358
2358
|
// deactivate / reactivate / sendPasswordReset / update) additionally need
|
|
2359
2359
|
// `users.write:*`, and the
|
|
2360
2360
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
2361
|
-
// call accepts `{ email, name,
|
|
2362
|
-
// AppUserInvite row (the
|
|
2361
|
+
// call accepts `{ email, name, group_ids?, send_invitation? }` and returns
|
|
2362
|
+
// the resulting AppUserInvite row (the host sends the email, unless
|
|
2363
|
+
// `send_invitation` is false). Mutating users from
|
|
2363
2364
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
2364
2365
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
2365
2366
|
// that only call read methods need only `users.read:*`.
|
|
@@ -2379,8 +2380,13 @@ const HOOKS = [
|
|
|
2379
2380
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
2380
2381
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
2381
2382
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
2382
|
-
"{ email, name, group_ids? } and returns the resulting
|
|
2383
|
-
"
|
|
2383
|
+
"{ email, name, group_ids?, send_invitation? } and returns the resulting " +
|
|
2384
|
+
"AppUserInvite row { id, email, name, expires_at, sent_at, status }. " +
|
|
2385
|
+
"The host emails the invitation at once unless send_invitation is " +
|
|
2386
|
+
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
2387
|
+
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
2388
|
+
"later. A send_invitation that is not a boolean rejects with " +
|
|
2389
|
+
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
2384
2390
|
"standard self-serve reset link to that user's own registered address " +
|
|
2385
2391
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
2386
2392
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -2419,7 +2425,7 @@ const HOOKS = [
|
|
|
2419
2425
|
error: "DirectoryError | null // { code, reason, message, retryable }",
|
|
2420
2426
|
refetch: "() => Promise<void>",
|
|
2421
2427
|
invite:
|
|
2422
|
-
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
2428
|
+
"({ email, name, group_ids?, send_invitation? }) => Promise<Invite> // send_invitation: false stores it unsent (status 'not_sent'); rejects with DirectoryError",
|
|
2423
2429
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2424
2430
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2425
2431
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -2637,7 +2643,8 @@ const HOOKS = [
|
|
|
2637
2643
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
2638
2644
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
2639
2645
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
2640
|
-
// caller's `users.write` SystemAcl capability.
|
|
2646
|
+
// caller's `users.write` SystemAcl capability. sc-9714: `resend` is also
|
|
2647
|
+
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
2641
2648
|
{
|
|
2642
2649
|
name: "useInvites",
|
|
2643
2650
|
signature: "useInvites(query)",
|
|
@@ -2647,13 +2654,23 @@ const HOOKS = [
|
|
|
2647
2654
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
2648
2655
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
2649
2656
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
2650
|
-
"res.data; rows are snake_case (email, status, expires_at, …)
|
|
2651
|
-
"status is the server-computed pending/accepted/revoked/expired
|
|
2652
|
-
"value.
|
|
2657
|
+
"res.data; rows are snake_case (email, status, sent_at, expires_at, …) " +
|
|
2658
|
+
"and status is the server-computed pending/accepted/revoked/expired/" +
|
|
2659
|
+
"not_sent value. not_sent is an invitation created with " +
|
|
2660
|
+
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
2661
|
+
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
2662
|
+
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
2663
|
+
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
2664
|
+
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
2665
|
+
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
2666
|
+
"'pending'. There is no separate send method; label the button " +
|
|
2667
|
+
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
2668
|
+
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
2653
2669
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
2654
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
2655
|
-
"status '
|
|
2656
|
-
"
|
|
2670
|
+
"{ status: 'pending' } for the usual outstanding-invite list and " +
|
|
2671
|
+
"{ status: 'not_sent' } for the prepared ones; " +
|
|
2672
|
+
"status 'open' is every invite neither accepted nor revoked (pending, " +
|
|
2673
|
+
"not_sent or expired). q is a " +
|
|
2657
2674
|
"case-insensitive substring match on the invite's name or email. " +
|
|
2658
2675
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
2659
2676
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -2670,12 +2687,12 @@ const HOOKS = [
|
|
|
2670
2687
|
"refetch on success.",
|
|
2671
2688
|
returnShape: {
|
|
2672
2689
|
invites:
|
|
2673
|
-
"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 }",
|
|
2690
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, sent_at, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; status is pending | accepted | revoked | expired | not_sent; unwrapped from { data, meta }",
|
|
2674
2691
|
loading: "boolean",
|
|
2675
2692
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
2676
2693
|
refetch: "() => Promise<void>",
|
|
2677
2694
|
resend:
|
|
2678
|
-
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
2695
|
+
"(inviteId) => Promise<Invite> // sends it (first send of a not_sent invite too); refetches; rejects with DirectoryError",
|
|
2679
2696
|
revoke:
|
|
2680
2697
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
2681
2698
|
},
|
|
@@ -4169,7 +4186,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4169
4186
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
4170
4187
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() }, " +
|
|
4171
4188
|
"presence: { online(tableId, recordIds) -> { data: [{ record_id, online_user_ids, online_count }] }, me() -> { show_online }, setShowOnline(bool) -> { show_online } } }. " +
|
|
4172
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke
|
|
4189
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites — resend also sends one created with send_invitation: false — gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope); presence backs useOnlineMembers() + useOnlineVisibility() (session-gated, no scope header). 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:*).",
|
|
4173
4190
|
required: true,
|
|
4174
4191
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object", presence: "object" },
|
|
4175
4192
|
},
|
|
@@ -6442,7 +6459,11 @@ const CONTRACT = deepFreeze({
|
|
|
6442
6459
|
// random id per row and may be `defaultLocked`.
|
|
6443
6460
|
// 1.215.0: additive (sc-13938) — `usePdfExport` `openPdf` / `exportToPdf`
|
|
6444
6461
|
// take `appendFileIds`: stored PDFs whose pages follow the rendered HTML.
|
|
6445
|
-
|
|
6462
|
+
// 1.216.0: additive (sc-14096) — reinstates 1.144.0, withdrawn in 1.155.0:
|
|
6463
|
+
// `useUsers().invite` takes `send_invitation` (false stores the invite
|
|
6464
|
+
// unsent), invite rows carry `sent_at`, `not_sent` is a status, and
|
|
6465
|
+
// `useInvites().resend` performs the first send.
|
|
6466
|
+
version: "1.216.0",
|
|
6446
6467
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6447
6468
|
hooks: HOOKS,
|
|
6448
6469
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -5977,7 +5977,7 @@ export function useDirectory(query) {
|
|
|
5977
5977
|
* invite, deactivate, reactivate, sendPasswordReset, update}` — `list` resolves to the `{ data, meta }`
|
|
5978
5978
|
* envelope VERBATIM, so we unwrap `res.data` (default `[]`). User rows are
|
|
5979
5979
|
* snake_case (`id`, `name`, `email`, `role`, `is_active`, …) and bodies
|
|
5980
|
-
* (e.g. `{ email, name, group_ids? }`) pass through verbatim. `query` is an
|
|
5980
|
+
* (e.g. `{ email, name, group_ids?, send_invitation? }`) pass through verbatim. `query` is an
|
|
5981
5981
|
* optional `{ q?, role?, is_active?, group_id?, limit?, offset? }`; `group_id`
|
|
5982
5982
|
* (the `id` of a `useGroups()` row) narrows the list to that group's members.
|
|
5983
5983
|
* The list refetches whenever `JSON.stringify(query)` changes; the imperative methods
|
|
@@ -6228,15 +6228,20 @@ export function useGroups(query) {
|
|
|
6228
6228
|
* `{ data, meta }` envelope VERBATIM, so we unwrap `res.data` (default `[]`).
|
|
6229
6229
|
*
|
|
6230
6230
|
* Rows are snake_case exactly as the backend projects them: `{ id, tenant_id,
|
|
6231
|
-
* email, name, group_ids, status, expires_at, accepted_at,
|
|
6232
|
-
* created_at, invited_by_studio_user_id }`. `status` is the
|
|
6233
|
-
* lifecycle value (`"pending" | "accepted" | "revoked" |
|
|
6234
|
-
* it over re-deriving expiry on the client,
|
|
6231
|
+
* email, name, group_ids, status, sent_at, expires_at, accepted_at,
|
|
6232
|
+
* revoked_at, created_at, invited_by_studio_user_id }`. `status` is the
|
|
6233
|
+
* server-computed lifecycle value (`"pending" | "accepted" | "revoked" |
|
|
6234
|
+
* "expired" | "not_sent"`) — prefer it over re-deriving expiry on the client,
|
|
6235
|
+
* whose clock may disagree.
|
|
6236
|
+
*
|
|
6237
|
+
* `not_sent` is an invite created with `send_invitation: false`: `sent_at` is
|
|
6238
|
+
* null and nothing was emailed. `resend(inviteId)` is the one call that sends
|
|
6239
|
+
* — a first send and a repeat send alike — and resolves the row as `pending`.
|
|
6235
6240
|
*
|
|
6236
6241
|
* `query` is an optional `{ status?, limit?, offset? }` passed through
|
|
6237
6242
|
* verbatim; `status` accepts `"pending" | "accepted" | "revoked" | "expired" |
|
|
6238
|
-
* "all"` and the endpoint defaults to `"all"`, so pass
|
|
6239
|
-
* for the usual "who hasn't accepted yet" list.
|
|
6243
|
+
* "not_sent" | "all"` and the endpoint defaults to `"all"`, so pass
|
|
6244
|
+
* `{ status: "pending" }` for the usual "who hasn't accepted yet" list.
|
|
6240
6245
|
*
|
|
6241
6246
|
* The WHOLE invite surface — list included — is gated on the `users.write`
|
|
6242
6247
|
* capability plus a signed `users.write:*` scope: a pending invite exposes
|
|
@@ -6303,7 +6308,8 @@ export function useInvites(query) {
|
|
|
6303
6308
|
}, [doFetch]);
|
|
6304
6309
|
|
|
6305
6310
|
// Both mutations re-list on success: revoking drops the row and resending
|
|
6306
|
-
// moves `expires_at
|
|
6311
|
+
// moves `expires_at` (and a first send's status), so a stale list would
|
|
6312
|
+
// show the operator the old state.
|
|
6307
6313
|
const resend = useCallback(
|
|
6308
6314
|
async (inviteId) => {
|
|
6309
6315
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -3289,13 +3289,25 @@ export interface InviteArgs {
|
|
|
3289
3289
|
email: string;
|
|
3290
3290
|
name?: string;
|
|
3291
3291
|
group_ids?: string[];
|
|
3292
|
+
/**
|
|
3293
|
+
* Defaults to `true`: the invitation is emailed at once. `false` stores it
|
|
3294
|
+
* without emailing anyone (`status: "not_sent"`); send it later with
|
|
3295
|
+
* `useInvites().resend(inviteId)`.
|
|
3296
|
+
*/
|
|
3297
|
+
send_invitation?: boolean;
|
|
3292
3298
|
}
|
|
3293
3299
|
|
|
3300
|
+
/**
|
|
3301
|
+
* When several apply the first of accepted, revoked, not_sent, expired,
|
|
3302
|
+
* pending wins. `not_sent` is an invitation created with
|
|
3303
|
+
* `send_invitation: false` that has not been emailed yet.
|
|
3304
|
+
*/
|
|
3294
3305
|
export type AppUserInviteStatus =
|
|
3295
3306
|
| "pending"
|
|
3296
3307
|
| "accepted"
|
|
3297
3308
|
| "revoked"
|
|
3298
|
-
| "expired"
|
|
3309
|
+
| "expired"
|
|
3310
|
+
| "not_sent";
|
|
3299
3311
|
|
|
3300
3312
|
export interface AppUserInviteRow {
|
|
3301
3313
|
id: string;
|
|
@@ -3310,7 +3322,9 @@ export interface AppUserInviteRow {
|
|
|
3310
3322
|
status: AppUserInviteStatus;
|
|
3311
3323
|
name?: string;
|
|
3312
3324
|
group_ids?: string[];
|
|
3313
|
-
|
|
3325
|
+
/** When the invitation email last left; `null` until it is first sent. */
|
|
3326
|
+
sent_at: string | null;
|
|
3327
|
+
/** Meaningless while `status` is `"not_sent"` — it restarts on the send. */
|
|
3314
3328
|
expires_at?: string;
|
|
3315
3329
|
accepted_at?: string | null;
|
|
3316
3330
|
revoked_at?: string | null;
|
|
@@ -3410,8 +3424,9 @@ export function useGroups(query?: GroupsQuery): GroupsApi;
|
|
|
3410
3424
|
|
|
3411
3425
|
export interface InvitesQuery {
|
|
3412
3426
|
/**
|
|
3413
|
-
* Defaults to "all" server-side; pass "pending" for outstanding invites
|
|
3414
|
-
* "
|
|
3427
|
+
* Defaults to "all" server-side; pass "pending" for outstanding invites and
|
|
3428
|
+
* "not_sent" for the ones prepared but not emailed yet.
|
|
3429
|
+
* "open" is "pending", "not_sent" or "expired": neither accepted nor revoked.
|
|
3415
3430
|
*/
|
|
3416
3431
|
status?: AppUserInviteStatus | "open" | "all";
|
|
3417
3432
|
/** Case-insensitive substring match on the invite's name or email. */
|
|
@@ -3432,7 +3447,12 @@ export interface InvitesApi {
|
|
|
3432
3447
|
loading: boolean;
|
|
3433
3448
|
error: DirectoryError | null;
|
|
3434
3449
|
refetch(): Promise<void>;
|
|
3435
|
-
/**
|
|
3450
|
+
/**
|
|
3451
|
+
* Email the invitation and restart its 7-day expiry. The one call that
|
|
3452
|
+
* sends: the first send of a `not_sent` invitation and a repeat send of a
|
|
3453
|
+
* sent one alike. Resolves the row with `status: "pending"`; refetches on
|
|
3454
|
+
* success.
|
|
3455
|
+
*/
|
|
3436
3456
|
resend(inviteId: string): Promise<AppUserInviteRow>;
|
|
3437
3457
|
/** Cancel a pending invitation. Refetches on success. */
|
|
3438
3458
|
revoke(inviteId: string): Promise<void>;
|
package/dist/property-schema.js
CHANGED
|
@@ -82,6 +82,9 @@ const VALID_TYPES = new Set([
|
|
|
82
82
|
"object", "array",
|
|
83
83
|
]);
|
|
84
84
|
|
|
85
|
+
/** The propertySchema field types, for documentation generated from this list. */
|
|
86
|
+
export const PROPERTY_TYPES = Object.freeze([...VALID_TYPES]);
|
|
87
|
+
|
|
85
88
|
function isPlainObject(v) {
|
|
86
89
|
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
87
90
|
}
|
package/dist/web-modal.js
CHANGED
|
@@ -59,6 +59,32 @@ function useCloseOnOutsideScroll(container, enabled, onCloseRef) {
|
|
|
59
59
|
}, [container, enabled, onCloseRef]);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// A dialog owns the screen, as on native: the page beneath must not
|
|
63
|
+
// scroll, even once the dialog's own body runs out of scroll.
|
|
64
|
+
let pageScrollLocks = 0;
|
|
65
|
+
let unlockedRootStyle = null;
|
|
66
|
+
|
|
67
|
+
function usePageScrollLock(enabled) {
|
|
68
|
+
React.useEffect(() => {
|
|
69
|
+
if (!enabled) return undefined;
|
|
70
|
+
const root = document.documentElement;
|
|
71
|
+
if (pageScrollLocks++ === 0) {
|
|
72
|
+
unlockedRootStyle = {
|
|
73
|
+
overflow: root.style.overflow,
|
|
74
|
+
scrollbarGutter: root.style.scrollbarGutter,
|
|
75
|
+
};
|
|
76
|
+
// Keep a visible scrollbar's space, so the page does not shift sideways.
|
|
77
|
+
if (window.innerWidth > root.clientWidth) root.style.scrollbarGutter = "stable";
|
|
78
|
+
root.style.overflow = "hidden";
|
|
79
|
+
}
|
|
80
|
+
return () => {
|
|
81
|
+
if (--pageScrollLocks > 0) return;
|
|
82
|
+
root.style.overflow = unlockedRootStyle.overflow;
|
|
83
|
+
root.style.scrollbarGutter = unlockedRootStyle.scrollbarGutter;
|
|
84
|
+
};
|
|
85
|
+
}, [enabled]);
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
function tabStops(dialog) {
|
|
63
89
|
return Array.from(dialog.querySelectorAll(FOCUSABLE)).filter(
|
|
64
90
|
(node) => node.getAttribute("aria-hidden") !== "true",
|
|
@@ -147,6 +173,8 @@ function ModalLayer({
|
|
|
147
173
|
}, [visible]);
|
|
148
174
|
const container = useBodyContainer(visible);
|
|
149
175
|
useCloseOnOutsideScroll(container, closesOnOutsideScroll, onCloseRef);
|
|
176
|
+
// An anchored panel closes on a scroll beneath instead.
|
|
177
|
+
usePageScrollLock(Boolean(container) && !closesOnOutsideScroll);
|
|
150
178
|
|
|
151
179
|
React.useEffect(() => {
|
|
152
180
|
const dialog = dialogRef.current;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.266.1",
|
|
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
|
@@ -2358,8 +2358,9 @@ const HOOKS = [
|
|
|
2358
2358
|
// deactivate / reactivate / sendPasswordReset / update) additionally need
|
|
2359
2359
|
// `users.write:*`, and the
|
|
2360
2360
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
2361
|
-
// call accepts `{ email, name,
|
|
2362
|
-
// AppUserInvite row (the
|
|
2361
|
+
// call accepts `{ email, name, group_ids?, send_invitation? }` and returns
|
|
2362
|
+
// the resulting AppUserInvite row (the host sends the email, unless
|
|
2363
|
+
// `send_invitation` is false). Mutating users from
|
|
2363
2364
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
2364
2365
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
2365
2366
|
// that only call read methods need only `users.read:*`.
|
|
@@ -2379,8 +2380,13 @@ const HOOKS = [
|
|
|
2379
2380
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
2380
2381
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
2381
2382
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
2382
|
-
"{ email, name, group_ids? } and returns the resulting
|
|
2383
|
-
"
|
|
2383
|
+
"{ email, name, group_ids?, send_invitation? } and returns the resulting " +
|
|
2384
|
+
"AppUserInvite row { id, email, name, expires_at, sent_at, status }. " +
|
|
2385
|
+
"The host emails the invitation at once unless send_invitation is " +
|
|
2386
|
+
"false: then nothing is emailed, the row comes back with sent_at null " +
|
|
2387
|
+
"and status 'not_sent', and useInvites().resend(inviteId) sends it " +
|
|
2388
|
+
"later. A send_invitation that is not a boolean rejects with " +
|
|
2389
|
+
"DirectoryError code VALIDATION. sendPasswordReset(userId) mails the " +
|
|
2384
2390
|
"standard self-serve reset link to that user's own registered address " +
|
|
2385
2391
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
2386
2392
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -2419,7 +2425,7 @@ const HOOKS = [
|
|
|
2419
2425
|
error: "DirectoryError | null // { code, reason, message, retryable }",
|
|
2420
2426
|
refetch: "() => Promise<void>",
|
|
2421
2427
|
invite:
|
|
2422
|
-
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
2428
|
+
"({ email, name, group_ids?, send_invitation? }) => Promise<Invite> // send_invitation: false stores it unsent (status 'not_sent'); rejects with DirectoryError",
|
|
2423
2429
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2424
2430
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2425
2431
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -2637,7 +2643,8 @@ const HOOKS = [
|
|
|
2637
2643
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
2638
2644
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
2639
2645
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
2640
|
-
// caller's `users.write` SystemAcl capability.
|
|
2646
|
+
// caller's `users.write` SystemAcl capability. sc-9714: `resend` is also
|
|
2647
|
+
// the first send of a `not_sent` invite — there is no separate `send`.
|
|
2641
2648
|
{
|
|
2642
2649
|
name: "useInvites",
|
|
2643
2650
|
signature: "useInvites(query)",
|
|
@@ -2647,13 +2654,23 @@ const HOOKS = [
|
|
|
2647
2654
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
2648
2655
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
2649
2656
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
2650
|
-
"res.data; rows are snake_case (email, status, expires_at, …)
|
|
2651
|
-
"status is the server-computed pending/accepted/revoked/expired
|
|
2652
|
-
"value.
|
|
2657
|
+
"res.data; rows are snake_case (email, status, sent_at, expires_at, …) " +
|
|
2658
|
+
"and status is the server-computed pending/accepted/revoked/expired/" +
|
|
2659
|
+
"not_sent value. not_sent is an invitation created with " +
|
|
2660
|
+
"send_invitation: false that has not been emailed yet: its sent_at is " +
|
|
2661
|
+
"null, it cannot be accepted until it is sent, and its expires_at " +
|
|
2662
|
+
"means nothing yet, so show no expiry for it. resend(inviteId) is the " +
|
|
2663
|
+
"ONE call that sends an invitation — the first send of a not_sent one " +
|
|
2664
|
+
"and a repeat send of a sent one alike: it emails the invitee, stamps " +
|
|
2665
|
+
"sent_at, restarts the 7-day expiry and resolves the row with status " +
|
|
2666
|
+
"'pending'. There is no separate send method; label the button " +
|
|
2667
|
+
"'Send invitation' on a not_sent row and 'Resend' on a pending one. " +
|
|
2668
|
+
"query is { status?, q?, app_user_id?, limit?, offset? } passed " +
|
|
2653
2669
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
2654
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
2655
|
-
"status '
|
|
2656
|
-
"
|
|
2670
|
+
"{ status: 'pending' } for the usual outstanding-invite list and " +
|
|
2671
|
+
"{ status: 'not_sent' } for the prepared ones; " +
|
|
2672
|
+
"status 'open' is every invite neither accepted nor revoked (pending, " +
|
|
2673
|
+
"not_sent or expired). q is a " +
|
|
2657
2674
|
"case-insensitive substring match on the invite's name or email. " +
|
|
2658
2675
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
2659
2676
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -2670,12 +2687,12 @@ const HOOKS = [
|
|
|
2670
2687
|
"refetch on success.",
|
|
2671
2688
|
returnShape: {
|
|
2672
2689
|
invites:
|
|
2673
|
-
"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 }",
|
|
2690
|
+
"Array<{ id, app_user_id, email, name, group_ids, status, sent_at, expires_at, accepted_at, revoked_at, created_at }> // snake_case rows; status is pending | accepted | revoked | expired | not_sent; unwrapped from { data, meta }",
|
|
2674
2691
|
loading: "boolean",
|
|
2675
2692
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
2676
2693
|
refetch: "() => Promise<void>",
|
|
2677
2694
|
resend:
|
|
2678
|
-
"(inviteId) => Promise<Invite> // refetches; rejects with DirectoryError",
|
|
2695
|
+
"(inviteId) => Promise<Invite> // sends it (first send of a not_sent invite too); refetches; rejects with DirectoryError",
|
|
2679
2696
|
revoke:
|
|
2680
2697
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
2681
2698
|
},
|
|
@@ -4169,7 +4186,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4169
4186
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
4170
4187
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() }, " +
|
|
4171
4188
|
"presence: { online(tableId, recordIds) -> { data: [{ record_id, online_user_ids, online_count }] }, me() -> { show_online }, setShowOnline(bool) -> { show_online } } }. " +
|
|
4172
|
-
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke
|
|
4189
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(); invites backs useInvites() (list/resend/revoke invites — resend also sends one created with send_invitation: false — gated by users.write:* + the users.write capability); bankid backs useBankIdLink() (REQ-BANKID-AUTH — self-service account linking, JWT-gated, no widget scope); presence backs useOnlineMembers() + useOnlineVisibility() (session-gated, no scope header). 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:*).",
|
|
4173
4190
|
required: true,
|
|
4174
4191
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object", presence: "object" },
|
|
4175
4192
|
},
|
|
@@ -6442,7 +6459,11 @@ const CONTRACT = deepFreeze({
|
|
|
6442
6459
|
// random id per row and may be `defaultLocked`.
|
|
6443
6460
|
// 1.215.0: additive (sc-13938) — `usePdfExport` `openPdf` / `exportToPdf`
|
|
6444
6461
|
// take `appendFileIds`: stored PDFs whose pages follow the rendered HTML.
|
|
6445
|
-
|
|
6462
|
+
// 1.216.0: additive (sc-14096) — reinstates 1.144.0, withdrawn in 1.155.0:
|
|
6463
|
+
// `useUsers().invite` takes `send_invitation` (false stores the invite
|
|
6464
|
+
// unsent), invite rows carry `sent_at`, `not_sent` is a status, and
|
|
6465
|
+
// `useInvites().resend` performs the first send.
|
|
6466
|
+
version: "1.216.0",
|
|
6446
6467
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6447
6468
|
hooks: HOOKS,
|
|
6448
6469
|
primitives: PRIMITIVES,
|