@colixsystems/widget-sdk 0.264.0 → 0.266.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 +45 -9
- package/dist/contract.cjs +52 -21
- package/dist/contract.js +52 -21
- package/dist/hooks.js +22 -9
- package/dist/index.d.ts +25 -5
- package/package.json +1 -1
- package/src/contract.js +52 -21
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.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**.
|
|
90
|
+
|
|
91
|
+
### What's new in 0.266.0 (contract 1.216.0)
|
|
92
|
+
|
|
93
|
+
**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`:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const { invite } = useUsers();
|
|
97
|
+
const { invites, resend } = useInvites({ status: "not_sent" });
|
|
98
|
+
|
|
99
|
+
const prepared = await invite({ email: "bo@example.com", name: "Bo", send_invitation: false });
|
|
100
|
+
// prepared.status === "not_sent", prepared.sent_at === null — nobody was emailed.
|
|
101
|
+
|
|
102
|
+
await resend(prepared.id); // emails it; the row is now "pending" with a fresh 7-day expiry
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- **`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`.
|
|
106
|
+
- **`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.
|
|
107
|
+
- **`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.
|
|
108
|
+
- **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.
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
### What's new in 0.265.0 (contract 1.215.0)
|
|
113
|
+
|
|
114
|
+
- **`usePdfExport` appends stored PDFs.** `openPdf(html, { appendFileIds })` and `exportToPdf(html, { appendFileIds })` take up to 20 Filestore file ids; every page of each PDF follows the rendered HTML, in order, on the web Player and in the native export alike (the merge runs server-side). An id the user cannot download, or a file that is not a readable PDF (password-protected, malformed), rejects the call with `err.details.file_id` naming it: 404 `FILE_NOT_FOUND`, 403 `FILE_DOWNLOAD_NOT_PERMITTED`, 422 `PDF_NOT_MERGEABLE`.
|
|
90
115
|
|
|
91
116
|
### What's new in 0.264.0 (contract 1.214.0)
|
|
92
117
|
|
|
@@ -1307,7 +1332,7 @@ An installed widget is not rebuilt or republished for this. The web Player hands
|
|
|
1307
1332
|
|
|
1308
1333
|
### What's new in 0.183.0 (contract 1.144.0)
|
|
1309
1334
|
|
|
1310
|
-
Send-later invitations (`send_invitation`, the `not_sent` status, `sent_at`). Withdrawn in 0.194.0.
|
|
1335
|
+
Send-later invitations (`send_invitation`, the `not_sent` status, `sent_at`). Withdrawn in 0.194.0, reinstated in 0.266.0.
|
|
1311
1336
|
|
|
1312
1337
|
### What's new in 0.182.0 (contract 1.143.0)
|
|
1313
1338
|
|
|
@@ -3345,11 +3370,13 @@ The matching manifest declares the scopes:
|
|
|
3345
3370
|
}
|
|
3346
3371
|
```
|
|
3347
3372
|
|
|
3373
|
+
`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)`.
|
|
3374
|
+
|
|
3348
3375
|
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.
|
|
3349
3376
|
|
|
3350
3377
|
## Listing pending invitations from a widget
|
|
3351
3378
|
|
|
3352
|
-
`useInvites(query?)` lists the workspace's invites and
|
|
3379
|
+
`useInvites(query?)` lists the workspace's invites and sends / revokes them:
|
|
3353
3380
|
|
|
3354
3381
|
```jsx
|
|
3355
3382
|
import { useInvites, View, Text, Pressable } from '@colixsystems/widget-sdk';
|
|
@@ -3364,7 +3391,9 @@ function PendingInvites() {
|
|
|
3364
3391
|
{invites.map((i) => (
|
|
3365
3392
|
<View key={i.id}>
|
|
3366
3393
|
<Text>{i.email} · {i.status}</Text>
|
|
3367
|
-
<Pressable onPress={() => resend(i.id).catch(() => {})}
|
|
3394
|
+
<Pressable onPress={() => resend(i.id).catch(() => {})}>
|
|
3395
|
+
<Text>{i.status === 'not_sent' ? 'Send invitation' : 'Resend'}</Text>
|
|
3396
|
+
</Pressable>
|
|
3368
3397
|
<Pressable onPress={() => revoke(i.id).catch(() => {})}><Text>Revoke</Text></Pressable>
|
|
3369
3398
|
</View>
|
|
3370
3399
|
))}
|
|
@@ -3373,9 +3402,16 @@ function PendingInvites() {
|
|
|
3373
3402
|
}
|
|
3374
3403
|
```
|
|
3375
3404
|
|
|
3376
|
-
Rows are snake_case: `{ id, app_user_id, email, name, group_ids, status,
|
|
3377
|
-
accepted_at, revoked_at, created_at }`.
|
|
3378
|
-
|
|
3405
|
+
Rows are snake_case: `{ id, app_user_id, email, name, group_ids, status, sent_at,
|
|
3406
|
+
expires_at, accepted_at, revoked_at, created_at }`. `status` is one of `pending`,
|
|
3407
|
+
`accepted`, `revoked`, `expired` or `not_sent`; trust the server-computed value rather
|
|
3408
|
+
than comparing `expires_at` against the device clock.
|
|
3409
|
+
|
|
3410
|
+
A `not_sent` row is an invitation created with `send_invitation: false`: nobody has
|
|
3411
|
+
been emailed and `sent_at` is `null`. `resend(inviteId)` is the one call that sends
|
|
3412
|
+
an invitation, a first send and a repeat send alike — it emails the invitee, stamps
|
|
3413
|
+
`sent_at`, restarts the 7-day expiry and resolves the row as `pending`. Pass
|
|
3414
|
+
`{ status: 'not_sent' }` to list only the prepared ones.
|
|
3379
3415
|
|
|
3380
3416
|
`query` also takes `q` (a case-insensitive substring match on the invite's name or
|
|
3381
3417
|
email) and `app_user_id`. Each row's `app_user_id` is the id the invitee's user account
|
|
@@ -3383,7 +3419,7 @@ will have once they accept. A `USER` column may hold that id before the person h
|
|
|
3383
3419
|
joined — it then reads back as `{ id, name, invited: true }`, with no `email` — and
|
|
3384
3420
|
`useInvites({ app_user_id: value.id, status: 'open', limit: 1 })` returns the invite
|
|
3385
3421
|
behind it. `status: 'open'` is every invite that is neither accepted nor revoked
|
|
3386
|
-
(`pending` or `expired`); a re-invite keeps the id, so without it earlier invites match too.
|
|
3422
|
+
(`pending`, `not_sent` or `expired`); a re-invite keeps the id, so without it earlier invites match too.
|
|
3387
3423
|
|
|
3388
3424
|
**The gate is `users.write`, not `users.read`.** A pending invite exposes the email of
|
|
3389
3425
|
someone who is not a member yet, so the backend gates the entire invite surface —
|
package/dist/contract.cjs
CHANGED
|
@@ -1539,7 +1539,7 @@ const HOOKS = [
|
|
|
1539
1539
|
optionalArgs: ["spaceType", "folderId"],
|
|
1540
1540
|
description:
|
|
1541
1541
|
"Render an HTML string to a PDF server-side, then either OPEN it or SAVE " +
|
|
1542
|
-
"it. `openPdf(html, { fileName? })` is THE way to give the user a PDF " +
|
|
1542
|
+
"it. `openPdf(html, { fileName?, appendFileIds? })` is THE way to give the user a PDF " +
|
|
1543
1543
|
"to view, print, download or share: it renders the HTML and opens the " +
|
|
1544
1544
|
"result — the browser's PDF viewer in a new tab on the web Player, the " +
|
|
1545
1545
|
"OS print / share sheet in the native export — and SAVES NOTHING, so " +
|
|
@@ -1555,19 +1555,27 @@ const HOOKS = [
|
|
|
1555
1555
|
"the end-user's Filestore space. The widget passes the SPACE " +
|
|
1556
1556
|
"(`{ spaceType, folderId? }`); the hook resolves owner_id from the host " +
|
|
1557
1557
|
"context and POSTs JSON through ctx.filestore.files.exportPdf. " +
|
|
1558
|
-
"`exportToPdf(html, { fileName?, folderId? })` resolves to the created " +
|
|
1558
|
+
"`exportToPdf(html, { fileName?, folderId?, appendFileIds? })` resolves to the created " +
|
|
1559
1559
|
"file row (mime_type `application/pdf`) or throws the wire error; " +
|
|
1560
1560
|
"`err.status` 403/404 means the destination denied the add (403 " +
|
|
1561
1561
|
"when the caller can see the position, 404 when it cannot), a 413 " +
|
|
1562
1562
|
"that the rendered " +
|
|
1563
1563
|
"PDF exceeded the size cap. The HTML is rendered by the host's PDF " +
|
|
1564
1564
|
"service (headless Chromium) — the SAME server-side pipeline on the web " +
|
|
1565
|
-
"Player and the native export, so no browser-only PDF library is used."
|
|
1565
|
+
"Player and the native export, so no browser-only PDF library is used. " +
|
|
1566
|
+
"To put a record's attached PDFs INSIDE the document, pass their Filestore " +
|
|
1567
|
+
"file ids as `appendFileIds` (at most 20) to either call: every page of " +
|
|
1568
|
+
"each file follows the rendered HTML, in the order given. Do not try to " +
|
|
1569
|
+
"embed a PDF through an <img>, <iframe> or link. Each id must be a PDF the " +
|
|
1570
|
+
"user may download, or the call rejects naming it in `err.details.file_id`: " +
|
|
1571
|
+
"404 FILE_NOT_FOUND, 403 FILE_DOWNLOAD_NOT_PERMITTED, or 422 " +
|
|
1572
|
+
"PDF_NOT_MERGEABLE (not a PDF, password-protected, or unreadable). On a " +
|
|
1573
|
+
"422, drop that id and call again so the rest of the report still opens.",
|
|
1566
1574
|
returnShape: {
|
|
1567
|
-
openPdf: "(html, { fileName? }) => Promise<void>",
|
|
1575
|
+
openPdf: "(html, { fileName?, appendFileIds? }) => Promise<void>",
|
|
1568
1576
|
opening: "boolean",
|
|
1569
1577
|
canOpenPdf: "boolean",
|
|
1570
|
-
exportToPdf: "(html, { fileName?, folderId? }) => Promise<FilestoreFile>",
|
|
1578
|
+
exportToPdf: "(html, { fileName?, folderId?, appendFileIds? }) => Promise<FilestoreFile>",
|
|
1571
1579
|
exporting: "boolean",
|
|
1572
1580
|
error: "Error | null",
|
|
1573
1581
|
lastExported: "FilestoreFile | null",
|
|
@@ -2353,8 +2361,9 @@ const HOOKS = [
|
|
|
2353
2361
|
// deactivate / reactivate / sendPasswordReset / update) additionally need
|
|
2354
2362
|
// `users.write:*`, and the
|
|
2355
2363
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
2356
|
-
// call accepts `{ email, name,
|
|
2357
|
-
// 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
|
|
2358
2367
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
2359
2368
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
2360
2369
|
// that only call read methods need only `users.read:*`.
|
|
@@ -2374,8 +2383,13 @@ const HOOKS = [
|
|
|
2374
2383
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
2375
2384
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
2376
2385
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
2377
|
-
"{ email, name, group_ids? } and returns the resulting
|
|
2378
|
-
"
|
|
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 " +
|
|
2379
2393
|
"standard self-serve reset link to that user's own registered address " +
|
|
2380
2394
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
2381
2395
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -2414,7 +2428,7 @@ const HOOKS = [
|
|
|
2414
2428
|
error: "DirectoryError | null // { code, reason, message, retryable }",
|
|
2415
2429
|
refetch: "() => Promise<void>",
|
|
2416
2430
|
invite:
|
|
2417
|
-
"({ 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",
|
|
2418
2432
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2419
2433
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2420
2434
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -2632,7 +2646,8 @@ const HOOKS = [
|
|
|
2632
2646
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
2633
2647
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
2634
2648
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
2635
|
-
// 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`.
|
|
2636
2651
|
{
|
|
2637
2652
|
name: "useInvites",
|
|
2638
2653
|
signature: "useInvites(query)",
|
|
@@ -2642,13 +2657,23 @@ const HOOKS = [
|
|
|
2642
2657
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
2643
2658
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
2644
2659
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
2645
|
-
"res.data; rows are snake_case (email, status, expires_at, …)
|
|
2646
|
-
"status is the server-computed pending/accepted/revoked/expired
|
|
2647
|
-
"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 " +
|
|
2648
2672
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
2649
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
2650
|
-
"status '
|
|
2651
|
-
"
|
|
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 " +
|
|
2652
2677
|
"case-insensitive substring match on the invite's name or email. " +
|
|
2653
2678
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
2654
2679
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -2665,12 +2690,12 @@ const HOOKS = [
|
|
|
2665
2690
|
"refetch on success.",
|
|
2666
2691
|
returnShape: {
|
|
2667
2692
|
invites:
|
|
2668
|
-
"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 }",
|
|
2669
2694
|
loading: "boolean",
|
|
2670
2695
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
2671
2696
|
refetch: "() => Promise<void>",
|
|
2672
2697
|
resend:
|
|
2673
|
-
"(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",
|
|
2674
2699
|
revoke:
|
|
2675
2700
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
2676
2701
|
},
|
|
@@ -4164,7 +4189,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4164
4189
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
4165
4190
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() }, " +
|
|
4166
4191
|
"presence: { online(tableId, recordIds) -> { data: [{ record_id, online_user_ids, online_count }] }, me() -> { show_online }, setShowOnline(bool) -> { show_online } } }. " +
|
|
4167
|
-
"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:*).",
|
|
4168
4193
|
required: true,
|
|
4169
4194
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object", presence: "object" },
|
|
4170
4195
|
},
|
|
@@ -6435,7 +6460,13 @@ const CONTRACT = deepFreeze({
|
|
|
6435
6460
|
// 1.214.0: additive (sc-13746) — a datastoreTemplate column may be a
|
|
6436
6461
|
// `UUID` (validated, stored lowercase); `"@uuid"` defaults it to a new
|
|
6437
6462
|
// random id per row and may be `defaultLocked`.
|
|
6438
|
-
|
|
6463
|
+
// 1.215.0: additive (sc-13938) — `usePdfExport` `openPdf` / `exportToPdf`
|
|
6464
|
+
// take `appendFileIds`: stored PDFs whose pages follow the rendered HTML.
|
|
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",
|
|
6439
6470
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6440
6471
|
hooks: HOOKS,
|
|
6441
6472
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1536,7 +1536,7 @@ const HOOKS = [
|
|
|
1536
1536
|
optionalArgs: ["spaceType", "folderId"],
|
|
1537
1537
|
description:
|
|
1538
1538
|
"Render an HTML string to a PDF server-side, then either OPEN it or SAVE " +
|
|
1539
|
-
"it. `openPdf(html, { fileName? })` is THE way to give the user a PDF " +
|
|
1539
|
+
"it. `openPdf(html, { fileName?, appendFileIds? })` is THE way to give the user a PDF " +
|
|
1540
1540
|
"to view, print, download or share: it renders the HTML and opens the " +
|
|
1541
1541
|
"result — the browser's PDF viewer in a new tab on the web Player, the " +
|
|
1542
1542
|
"OS print / share sheet in the native export — and SAVES NOTHING, so " +
|
|
@@ -1552,19 +1552,27 @@ const HOOKS = [
|
|
|
1552
1552
|
"the end-user's Filestore space. The widget passes the SPACE " +
|
|
1553
1553
|
"(`{ spaceType, folderId? }`); the hook resolves owner_id from the host " +
|
|
1554
1554
|
"context and POSTs JSON through ctx.filestore.files.exportPdf. " +
|
|
1555
|
-
"`exportToPdf(html, { fileName?, folderId? })` resolves to the created " +
|
|
1555
|
+
"`exportToPdf(html, { fileName?, folderId?, appendFileIds? })` resolves to the created " +
|
|
1556
1556
|
"file row (mime_type `application/pdf`) or throws the wire error; " +
|
|
1557
1557
|
"`err.status` 403/404 means the destination denied the add (403 " +
|
|
1558
1558
|
"when the caller can see the position, 404 when it cannot), a 413 " +
|
|
1559
1559
|
"that the rendered " +
|
|
1560
1560
|
"PDF exceeded the size cap. The HTML is rendered by the host's PDF " +
|
|
1561
1561
|
"service (headless Chromium) — the SAME server-side pipeline on the web " +
|
|
1562
|
-
"Player and the native export, so no browser-only PDF library is used."
|
|
1562
|
+
"Player and the native export, so no browser-only PDF library is used. " +
|
|
1563
|
+
"To put a record's attached PDFs INSIDE the document, pass their Filestore " +
|
|
1564
|
+
"file ids as `appendFileIds` (at most 20) to either call: every page of " +
|
|
1565
|
+
"each file follows the rendered HTML, in the order given. Do not try to " +
|
|
1566
|
+
"embed a PDF through an <img>, <iframe> or link. Each id must be a PDF the " +
|
|
1567
|
+
"user may download, or the call rejects naming it in `err.details.file_id`: " +
|
|
1568
|
+
"404 FILE_NOT_FOUND, 403 FILE_DOWNLOAD_NOT_PERMITTED, or 422 " +
|
|
1569
|
+
"PDF_NOT_MERGEABLE (not a PDF, password-protected, or unreadable). On a " +
|
|
1570
|
+
"422, drop that id and call again so the rest of the report still opens.",
|
|
1563
1571
|
returnShape: {
|
|
1564
|
-
openPdf: "(html, { fileName? }) => Promise<void>",
|
|
1572
|
+
openPdf: "(html, { fileName?, appendFileIds? }) => Promise<void>",
|
|
1565
1573
|
opening: "boolean",
|
|
1566
1574
|
canOpenPdf: "boolean",
|
|
1567
|
-
exportToPdf: "(html, { fileName?, folderId? }) => Promise<FilestoreFile>",
|
|
1575
|
+
exportToPdf: "(html, { fileName?, folderId?, appendFileIds? }) => Promise<FilestoreFile>",
|
|
1568
1576
|
exporting: "boolean",
|
|
1569
1577
|
error: "Error | null",
|
|
1570
1578
|
lastExported: "FilestoreFile | null",
|
|
@@ -2350,8 +2358,9 @@ const HOOKS = [
|
|
|
2350
2358
|
// deactivate / reactivate / sendPasswordReset / update) additionally need
|
|
2351
2359
|
// `users.write:*`, and the
|
|
2352
2360
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
2353
|
-
// call accepts `{ email, name,
|
|
2354
|
-
// 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
|
|
2355
2364
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
2356
2365
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
2357
2366
|
// that only call read methods need only `users.read:*`.
|
|
@@ -2371,8 +2380,13 @@ const HOOKS = [
|
|
|
2371
2380
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
2372
2381
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
2373
2382
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
2374
|
-
"{ email, name, group_ids? } and returns the resulting
|
|
2375
|
-
"
|
|
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 " +
|
|
2376
2390
|
"standard self-serve reset link to that user's own registered address " +
|
|
2377
2391
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
2378
2392
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -2411,7 +2425,7 @@ const HOOKS = [
|
|
|
2411
2425
|
error: "DirectoryError | null // { code, reason, message, retryable }",
|
|
2412
2426
|
refetch: "() => Promise<void>",
|
|
2413
2427
|
invite:
|
|
2414
|
-
"({ 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",
|
|
2415
2429
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2416
2430
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2417
2431
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -2629,7 +2643,8 @@ const HOOKS = [
|
|
|
2629
2643
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
2630
2644
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
2631
2645
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
2632
|
-
// 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`.
|
|
2633
2648
|
{
|
|
2634
2649
|
name: "useInvites",
|
|
2635
2650
|
signature: "useInvites(query)",
|
|
@@ -2639,13 +2654,23 @@ const HOOKS = [
|
|
|
2639
2654
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
2640
2655
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
2641
2656
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
2642
|
-
"res.data; rows are snake_case (email, status, expires_at, …)
|
|
2643
|
-
"status is the server-computed pending/accepted/revoked/expired
|
|
2644
|
-
"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 " +
|
|
2645
2669
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
2646
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
2647
|
-
"status '
|
|
2648
|
-
"
|
|
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 " +
|
|
2649
2674
|
"case-insensitive substring match on the invite's name or email. " +
|
|
2650
2675
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
2651
2676
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -2662,12 +2687,12 @@ const HOOKS = [
|
|
|
2662
2687
|
"refetch on success.",
|
|
2663
2688
|
returnShape: {
|
|
2664
2689
|
invites:
|
|
2665
|
-
"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 }",
|
|
2666
2691
|
loading: "boolean",
|
|
2667
2692
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
2668
2693
|
refetch: "() => Promise<void>",
|
|
2669
2694
|
resend:
|
|
2670
|
-
"(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",
|
|
2671
2696
|
revoke:
|
|
2672
2697
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
2673
2698
|
},
|
|
@@ -4161,7 +4186,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4161
4186
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
4162
4187
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() }, " +
|
|
4163
4188
|
"presence: { online(tableId, recordIds) -> { data: [{ record_id, online_user_ids, online_count }] }, me() -> { show_online }, setShowOnline(bool) -> { show_online } } }. " +
|
|
4164
|
-
"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:*).",
|
|
4165
4190
|
required: true,
|
|
4166
4191
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object", presence: "object" },
|
|
4167
4192
|
},
|
|
@@ -6432,7 +6457,13 @@ const CONTRACT = deepFreeze({
|
|
|
6432
6457
|
// 1.214.0: additive (sc-13746) — a datastoreTemplate column may be a
|
|
6433
6458
|
// `UUID` (validated, stored lowercase); `"@uuid"` defaults it to a new
|
|
6434
6459
|
// random id per row and may be `defaultLocked`.
|
|
6435
|
-
|
|
6460
|
+
// 1.215.0: additive (sc-13938) — `usePdfExport` `openPdf` / `exportToPdf`
|
|
6461
|
+
// take `appendFileIds`: stored PDFs whose pages follow the rendered HTML.
|
|
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",
|
|
6436
6467
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6437
6468
|
hooks: HOOKS,
|
|
6438
6469
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -5171,7 +5171,7 @@ export function usePdfExport(options) {
|
|
|
5171
5171
|
try {
|
|
5172
5172
|
await viewer.open({
|
|
5173
5173
|
fileName: _pdfFileName(overrides && overrides.fileName),
|
|
5174
|
-
load: () => files.renderPdf(
|
|
5174
|
+
load: () => files.renderPdf(_pdfRenderBody(html, overrides)),
|
|
5175
5175
|
});
|
|
5176
5176
|
} catch (err) {
|
|
5177
5177
|
setError(err);
|
|
@@ -5204,6 +5204,7 @@ export function usePdfExport(options) {
|
|
|
5204
5204
|
};
|
|
5205
5205
|
if (folderId) body.folder_id = folderId;
|
|
5206
5206
|
if (fileName) body.file_name = fileName;
|
|
5207
|
+
Object.assign(body, _pdfRenderBody(html, overrides));
|
|
5207
5208
|
setExporting(true);
|
|
5208
5209
|
setError(null);
|
|
5209
5210
|
try {
|
|
@@ -5231,6 +5232,12 @@ export function usePdfExport(options) {
|
|
|
5231
5232
|
};
|
|
5232
5233
|
}
|
|
5233
5234
|
|
|
5235
|
+
// `appendFileIds`: stored PDFs whose pages follow the rendered HTML.
|
|
5236
|
+
function _pdfRenderBody(html, overrides) {
|
|
5237
|
+
const ids = overrides && overrides.appendFileIds;
|
|
5238
|
+
return Array.isArray(ids) && ids.length > 0 ? { html, append_file_ids: ids } : { html };
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5234
5241
|
function _canOpenPdf(viewer) {
|
|
5235
5242
|
return Boolean(
|
|
5236
5243
|
viewer &&
|
|
@@ -5970,7 +5977,7 @@ export function useDirectory(query) {
|
|
|
5970
5977
|
* invite, deactivate, reactivate, sendPasswordReset, update}` — `list` resolves to the `{ data, meta }`
|
|
5971
5978
|
* envelope VERBATIM, so we unwrap `res.data` (default `[]`). User rows are
|
|
5972
5979
|
* snake_case (`id`, `name`, `email`, `role`, `is_active`, …) and bodies
|
|
5973
|
-
* (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
|
|
5974
5981
|
* optional `{ q?, role?, is_active?, group_id?, limit?, offset? }`; `group_id`
|
|
5975
5982
|
* (the `id` of a `useGroups()` row) narrows the list to that group's members.
|
|
5976
5983
|
* The list refetches whenever `JSON.stringify(query)` changes; the imperative methods
|
|
@@ -6221,15 +6228,20 @@ export function useGroups(query) {
|
|
|
6221
6228
|
* `{ data, meta }` envelope VERBATIM, so we unwrap `res.data` (default `[]`).
|
|
6222
6229
|
*
|
|
6223
6230
|
* Rows are snake_case exactly as the backend projects them: `{ id, tenant_id,
|
|
6224
|
-
* email, name, group_ids, status, expires_at, accepted_at,
|
|
6225
|
-
* created_at, invited_by_studio_user_id }`. `status` is the
|
|
6226
|
-
* lifecycle value (`"pending" | "accepted" | "revoked" |
|
|
6227
|
-
* 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`.
|
|
6228
6240
|
*
|
|
6229
6241
|
* `query` is an optional `{ status?, limit?, offset? }` passed through
|
|
6230
6242
|
* verbatim; `status` accepts `"pending" | "accepted" | "revoked" | "expired" |
|
|
6231
|
-
* "all"` and the endpoint defaults to `"all"`, so pass
|
|
6232
|
-
* 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.
|
|
6233
6245
|
*
|
|
6234
6246
|
* The WHOLE invite surface — list included — is gated on the `users.write`
|
|
6235
6247
|
* capability plus a signed `users.write:*` scope: a pending invite exposes
|
|
@@ -6296,7 +6308,8 @@ export function useInvites(query) {
|
|
|
6296
6308
|
}, [doFetch]);
|
|
6297
6309
|
|
|
6298
6310
|
// Both mutations re-list on success: revoking drops the row and resending
|
|
6299
|
-
// 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.
|
|
6300
6313
|
const resend = useCallback(
|
|
6301
6314
|
async (inviteId) => {
|
|
6302
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.266.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
|
@@ -1536,7 +1536,7 @@ const HOOKS = [
|
|
|
1536
1536
|
optionalArgs: ["spaceType", "folderId"],
|
|
1537
1537
|
description:
|
|
1538
1538
|
"Render an HTML string to a PDF server-side, then either OPEN it or SAVE " +
|
|
1539
|
-
"it. `openPdf(html, { fileName? })` is THE way to give the user a PDF " +
|
|
1539
|
+
"it. `openPdf(html, { fileName?, appendFileIds? })` is THE way to give the user a PDF " +
|
|
1540
1540
|
"to view, print, download or share: it renders the HTML and opens the " +
|
|
1541
1541
|
"result — the browser's PDF viewer in a new tab on the web Player, the " +
|
|
1542
1542
|
"OS print / share sheet in the native export — and SAVES NOTHING, so " +
|
|
@@ -1552,19 +1552,27 @@ const HOOKS = [
|
|
|
1552
1552
|
"the end-user's Filestore space. The widget passes the SPACE " +
|
|
1553
1553
|
"(`{ spaceType, folderId? }`); the hook resolves owner_id from the host " +
|
|
1554
1554
|
"context and POSTs JSON through ctx.filestore.files.exportPdf. " +
|
|
1555
|
-
"`exportToPdf(html, { fileName?, folderId? })` resolves to the created " +
|
|
1555
|
+
"`exportToPdf(html, { fileName?, folderId?, appendFileIds? })` resolves to the created " +
|
|
1556
1556
|
"file row (mime_type `application/pdf`) or throws the wire error; " +
|
|
1557
1557
|
"`err.status` 403/404 means the destination denied the add (403 " +
|
|
1558
1558
|
"when the caller can see the position, 404 when it cannot), a 413 " +
|
|
1559
1559
|
"that the rendered " +
|
|
1560
1560
|
"PDF exceeded the size cap. The HTML is rendered by the host's PDF " +
|
|
1561
1561
|
"service (headless Chromium) — the SAME server-side pipeline on the web " +
|
|
1562
|
-
"Player and the native export, so no browser-only PDF library is used."
|
|
1562
|
+
"Player and the native export, so no browser-only PDF library is used. " +
|
|
1563
|
+
"To put a record's attached PDFs INSIDE the document, pass their Filestore " +
|
|
1564
|
+
"file ids as `appendFileIds` (at most 20) to either call: every page of " +
|
|
1565
|
+
"each file follows the rendered HTML, in the order given. Do not try to " +
|
|
1566
|
+
"embed a PDF through an <img>, <iframe> or link. Each id must be a PDF the " +
|
|
1567
|
+
"user may download, or the call rejects naming it in `err.details.file_id`: " +
|
|
1568
|
+
"404 FILE_NOT_FOUND, 403 FILE_DOWNLOAD_NOT_PERMITTED, or 422 " +
|
|
1569
|
+
"PDF_NOT_MERGEABLE (not a PDF, password-protected, or unreadable). On a " +
|
|
1570
|
+
"422, drop that id and call again so the rest of the report still opens.",
|
|
1563
1571
|
returnShape: {
|
|
1564
|
-
openPdf: "(html, { fileName? }) => Promise<void>",
|
|
1572
|
+
openPdf: "(html, { fileName?, appendFileIds? }) => Promise<void>",
|
|
1565
1573
|
opening: "boolean",
|
|
1566
1574
|
canOpenPdf: "boolean",
|
|
1567
|
-
exportToPdf: "(html, { fileName?, folderId? }) => Promise<FilestoreFile>",
|
|
1575
|
+
exportToPdf: "(html, { fileName?, folderId?, appendFileIds? }) => Promise<FilestoreFile>",
|
|
1568
1576
|
exporting: "boolean",
|
|
1569
1577
|
error: "Error | null",
|
|
1570
1578
|
lastExported: "FilestoreFile | null",
|
|
@@ -2350,8 +2358,9 @@ const HOOKS = [
|
|
|
2350
2358
|
// deactivate / reactivate / sendPasswordReset / update) additionally need
|
|
2351
2359
|
// `users.write:*`, and the
|
|
2352
2360
|
// destructive `remove` needs `users.delete:*` (SC-902). The `invite`
|
|
2353
|
-
// call accepts `{ email, name,
|
|
2354
|
-
// 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
|
|
2355
2364
|
// a widget requires the corresponding SystemAcl capability grant in the
|
|
2356
2365
|
// tenant (`users.write` for edits, `users.delete` for removal); widgets
|
|
2357
2366
|
// that only call read methods need only `users.read:*`.
|
|
@@ -2371,8 +2380,13 @@ const HOOKS = [
|
|
|
2371
2380
|
"scope; edit-style mutations (invite/deactivate/reactivate/" +
|
|
2372
2381
|
"sendPasswordReset/update) need users.write:*, and the destructive remove() " +
|
|
2373
2382
|
"additionally needs users.delete:* (SC-902). The `invite` call accepts " +
|
|
2374
|
-
"{ email, name, group_ids? } and returns the resulting
|
|
2375
|
-
"
|
|
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 " +
|
|
2376
2390
|
"standard self-serve reset link to that user's own registered address " +
|
|
2377
2391
|
"and resolves { sent, email_masked } — never the token or the link, so " +
|
|
2378
2392
|
"it cannot be used to sign in as them. It rejects with a DirectoryError " +
|
|
@@ -2411,7 +2425,7 @@ const HOOKS = [
|
|
|
2411
2425
|
error: "DirectoryError | null // { code, reason, message, retryable }",
|
|
2412
2426
|
refetch: "() => Promise<void>",
|
|
2413
2427
|
invite:
|
|
2414
|
-
"({ 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",
|
|
2415
2429
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2416
2430
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
2417
2431
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
@@ -2629,7 +2643,8 @@ const HOOKS = [
|
|
|
2629
2643
|
// sc-5097 — pending AppUser invite administration. Returns
|
|
2630
2644
|
// `{ invites, loading, error, refetch, resend, revoke }`. Reads need
|
|
2631
2645
|
// `invites.read:*`; resend/revoke need `invites.write:*` plus the
|
|
2632
|
-
// 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`.
|
|
2633
2648
|
{
|
|
2634
2649
|
name: "useInvites",
|
|
2635
2650
|
signature: "useInvites(query)",
|
|
@@ -2639,13 +2654,23 @@ const HOOKS = [
|
|
|
2639
2654
|
"directory-client at ctx.directory.invites.{list,resend,revoke}. " +
|
|
2640
2655
|
"Returns { invites, loading, error, refetch, resend, revoke }. list " +
|
|
2641
2656
|
"returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
2642
|
-
"res.data; rows are snake_case (email, status, expires_at, …)
|
|
2643
|
-
"status is the server-computed pending/accepted/revoked/expired
|
|
2644
|
-
"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 " +
|
|
2645
2669
|
"verbatim; the endpoint defaults to status 'all', so pass " +
|
|
2646
|
-
"{ status: 'pending' } for the usual outstanding-invite list
|
|
2647
|
-
"status '
|
|
2648
|
-
"
|
|
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 " +
|
|
2649
2674
|
"case-insensitive substring match on the invite's name or email. " +
|
|
2650
2675
|
"Each row's app_user_id is the id the invitee's user account will " +
|
|
2651
2676
|
"have once they accept; a USER column may already hold it, and " +
|
|
@@ -2662,12 +2687,12 @@ const HOOKS = [
|
|
|
2662
2687
|
"refetch on success.",
|
|
2663
2688
|
returnShape: {
|
|
2664
2689
|
invites:
|
|
2665
|
-
"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 }",
|
|
2666
2691
|
loading: "boolean",
|
|
2667
2692
|
error: "DirectoryError | null // { code, message, retryable }",
|
|
2668
2693
|
refetch: "() => Promise<void>",
|
|
2669
2694
|
resend:
|
|
2670
|
-
"(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",
|
|
2671
2696
|
revoke:
|
|
2672
2697
|
"(inviteId) => Promise<void> // refetches; rejects with DirectoryError",
|
|
2673
2698
|
},
|
|
@@ -4161,7 +4186,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4161
4186
|
"invites: { list(), revoke(id), resend(id) }, " +
|
|
4162
4187
|
"bankid: { status() -> { linked, available }, startLink() -> { order_ref, qr, ... }, collect(orderRef), cancel(orderRef), unlink() }, " +
|
|
4163
4188
|
"presence: { online(tableId, recordIds) -> { data: [{ record_id, online_user_ids, online_count }] }, me() -> { show_online }, setShowOnline(bool) -> { show_online } } }. " +
|
|
4164
|
-
"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:*).",
|
|
4165
4190
|
required: true,
|
|
4166
4191
|
fields: { users: "object", groups: "object", invites: "object", bankid: "object", presence: "object" },
|
|
4167
4192
|
},
|
|
@@ -6432,7 +6457,13 @@ const CONTRACT = deepFreeze({
|
|
|
6432
6457
|
// 1.214.0: additive (sc-13746) — a datastoreTemplate column may be a
|
|
6433
6458
|
// `UUID` (validated, stored lowercase); `"@uuid"` defaults it to a new
|
|
6434
6459
|
// random id per row and may be `defaultLocked`.
|
|
6435
|
-
|
|
6460
|
+
// 1.215.0: additive (sc-13938) — `usePdfExport` `openPdf` / `exportToPdf`
|
|
6461
|
+
// take `appendFileIds`: stored PDFs whose pages follow the rendered HTML.
|
|
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",
|
|
6436
6467
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6437
6468
|
hooks: HOOKS,
|
|
6438
6469
|
primitives: PRIMITIVES,
|