@colixsystems/widget-sdk 0.53.0 → 0.55.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 +13 -4
- package/dist/contract.cjs +43 -3
- package/dist/contract.js +43 -3
- package/dist/hooks.js +82 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/property-schema.js +13 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
19
19
|
| ----- | ---------------- | ------- | ------------- |
|
|
20
20
|
| **CORE** | `useTheme()` | `{ colors, spacing, radii, typography }` | `ctx.workspace.theme` — no scope |
|
|
21
21
|
| **CORE** | `useWidgetStyle()` | `{ [styleField]: value }` | `ctx.props.style` — no scope. The author-set per-widget style values declared in `manifest.styleSchema`; apply each onto whatever element you choose. |
|
|
22
|
-
| **CORE** | `useUser()` | `{ id, email,
|
|
22
|
+
| **CORE** | `useUser()` | `{ id, email, displayName, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope |
|
|
23
23
|
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute }` | `ctx.navigation` — no scope (external URLs use the `Linking` primitive) |
|
|
24
24
|
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope |
|
|
25
25
|
| **CORE** | `useChildRenderer()` | `{ renderNode(node) }` | `ctx.renderer` — no scope (prefer the `WidgetTree` component) |
|
|
@@ -40,6 +40,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
40
40
|
| **DIRECTORY** | `useUsers(query?)` | `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` | `directory.users.*` — `users.read:*` (edits also `users.write:*`; `remove()` also `users.delete:*`) |
|
|
41
41
|
| **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
42
42
|
| **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
|
|
43
|
+
| **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId? })` | `{ exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.exportPdf` — `files.write:*`. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF server-side and saves it as a file (`application/pdf`); same server-side renderer on web + native. |
|
|
43
44
|
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser` |
|
|
44
45
|
| **NOTIFICATIONS** (`ctx.notifications`) | `useSendNotification()` | `{ send, sending, error }` | `ctx.notifications.send` — `notifications.send:appUser`. `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace; call from an event handler (never render); rejects with `NotificationError`. |
|
|
45
46
|
|
|
@@ -51,7 +52,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
51
52
|
|
|
52
53
|
## Status
|
|
53
54
|
|
|
54
|
-
`v0.
|
|
55
|
+
`v0.55.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**.
|
|
56
|
+
|
|
57
|
+
### What's new in 0.55.0
|
|
58
|
+
|
|
59
|
+
**Dynamic record selection for `valueRef` (sc-2327).** The `valueRef` binding gains an optional `mode` field: `"static"` (the default — pin a specific `recordId`, the only prior behaviour) or `"latest"` (resolve the most recently created row live, sorting on the host-managed `created_at` descending with `limit: 1`; `recordId` is ignored). The built-in Field Value widget reads it to offer a "Latest entry" that updates as records are added, with no per-host code — the same baked widget source and the same injected `@colixsystems/datastore-client` run on the web Player and the native Expo export. The `ValueRefBinding` type adds `mode?: "static" | "latest"`. Existing bindings carry no `mode` and read as static. `CONTRACT.version` → `1.39.0`. Additive — no existing field changed shape.
|
|
60
|
+
|
|
61
|
+
### What's new in 0.54.0
|
|
62
|
+
|
|
63
|
+
**Generate & save PDFs from a widget (sc-2314).** New `usePdfExport({ spaceType, folderId? })` hook. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF **server-side** (the platform's headless-Chromium pipeline) and saves it into the end-user's Filestore via `ctx.filestore.files.exportPdf`, resolving to the created file row (`application/pdf`). It reuses the filestore owner_id resolution + per-folder write gate and the existing `files.write:*` scope. Because the rendering is server-side, the capability behaves identically on the web Player and the native Expo export — no browser-only PDF library is added to the vetted set. Pairs with `@colixsystems/filestore-client@0.6.0`'s new `files.exportPdf(...)`. `CONTRACT.version` → `1.38.0`. Additive — no existing hook, primitive, manifest field, or token changed shape.
|
|
55
64
|
|
|
56
65
|
### What's new in 0.53.0
|
|
57
66
|
|
|
@@ -278,7 +287,7 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
|
|
|
278
287
|
|
|
279
288
|
- **The SDK no longer owns any data facade.** The bespoke per-hook facades that used to live on `WidgetContext` (`ctx.datastore` as an opaque host object, `ctx.directory.listUsers`, `ctx.users`, `ctx.groups`, `ctx.recordPermissions`, `ctx.assets.get`) are replaced by four host-instantiated, host-injected domain clients: `ctx.datastore` (`@colixsystems/datastore-client`), `ctx.directory` (`@colixsystems/directory-client`), `ctx.assets` (`@colixsystems/assets-client`, flattened), `ctx.payments` (`@colixsystems/payments-client`). The SDK imports none of them and ships no HTTP.
|
|
280
289
|
- **`ctx.recordPermissions`, `ctx.users`, `ctx.groups` are removed.** Per-record permission management moved under `ctx.datastore.records(tableId).permissions(recordId)`; user / group administration moved under `ctx.directory.users` / `ctx.directory.groups`. The hooks (`useRecordPermissions`, `useUsers`, `useGroups`) keep the same names and signatures — only the client slice they read changed.
|
|
281
|
-
- **snake_case end to end, no client-side transform.** Clients send and return snake_case verbatim (`group_ids`, `can_read`, `is_active`, `amount_cents`, `data_type`, `created_at`, …). The SDK passes bodies straight through and unwraps the `{ data, meta }` list envelope without renaming a single field.
|
|
290
|
+
- **snake_case end to end, no client-side transform.** Clients send and return snake_case verbatim (`group_ids`, `can_read`, `is_active`, `amount_cents`, `data_type`, `created_at`, …). The SDK passes bodies straight through and unwraps the `{ data, meta }` list envelope without renaming a single field. Wire-payload hook return rows are therefore snake_case (`is_active`, `member_count`, and `useRecordPermissions` rows carry `user_id` / `group_id` / `can_read` / `can_write` / `can_delete` / `can_grant`). The one exception is `useUser()`: it reads the host-built `ctx.user` context object, not a wire payload, so its fields are **camelCase** (`displayName`, `groupIds`).
|
|
282
291
|
- **Companion package versions:** `datastore-client 0.5.0`, `assets-client 0.4.0`, `directory-client 0.1.0`, `payments-client 0.1.0`.
|
|
283
292
|
- **`CONTRACT.version` → `1.9.0`.** Breaking for `WidgetContext` consumers (removed slices, renamed wire fields); the hook export surface is unchanged.
|
|
284
293
|
|
|
@@ -423,7 +432,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
423
432
|
|
|
424
433
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
425
434
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
426
|
-
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email,
|
|
435
|
+
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
|
|
427
436
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
428
437
|
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
429
438
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
package/dist/contract.cjs
CHANGED
|
@@ -235,6 +235,29 @@ const HOOKS = [
|
|
|
235
235
|
requiredContextSlice: ["filestore.files"],
|
|
236
236
|
scopes: ["files.write:*"],
|
|
237
237
|
},
|
|
238
|
+
{
|
|
239
|
+
name: "usePdfExport",
|
|
240
|
+
signature: "usePdfExport({ spaceType, folderId? })",
|
|
241
|
+
description:
|
|
242
|
+
"Render an HTML string to a PDF server-side and SAVE it as a file in " +
|
|
243
|
+
"the end-user's Filestore space. The widget passes the SPACE " +
|
|
244
|
+
"(`{ spaceType, folderId? }`); the hook resolves owner_id from the host " +
|
|
245
|
+
"context and POSTs JSON through ctx.filestore.files.exportPdf. " +
|
|
246
|
+
"`exportToPdf(html, { fileName?, folderId? })` resolves to the created " +
|
|
247
|
+
"file row (mime_type `application/pdf`) or throws the wire error; a 404 " +
|
|
248
|
+
"means the destination folder denied a write, a 413 that the rendered " +
|
|
249
|
+
"PDF exceeded the size cap. The HTML is rendered by the host's PDF " +
|
|
250
|
+
"service (headless Chromium) — the SAME server-side pipeline on the web " +
|
|
251
|
+
"Player and the native export, so no browser-only PDF library is used.",
|
|
252
|
+
returnShape: {
|
|
253
|
+
exportToPdf: "(html, { fileName?, folderId? }) => Promise<FilestoreFile>",
|
|
254
|
+
exporting: "boolean",
|
|
255
|
+
error: "Error | null",
|
|
256
|
+
lastExported: "FilestoreFile | null",
|
|
257
|
+
},
|
|
258
|
+
requiredContextSlice: ["filestore.files"],
|
|
259
|
+
scopes: ["files.write:*"],
|
|
260
|
+
},
|
|
238
261
|
{
|
|
239
262
|
name: "useFilestoreFolders",
|
|
240
263
|
signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
|
|
@@ -975,9 +998,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
975
998
|
},
|
|
976
999
|
user: {
|
|
977
1000
|
description:
|
|
978
|
-
"Signed-in user, host-
|
|
1001
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. Not a data-client.",
|
|
979
1002
|
required: true,
|
|
980
|
-
fields: { id: "string", email: "string",
|
|
1003
|
+
fields: { id: "string", email: "string", displayName: "string" },
|
|
981
1004
|
},
|
|
982
1005
|
workspace: {
|
|
983
1006
|
description:
|
|
@@ -1824,7 +1847,24 @@ const CONTRACT = deepFreeze({
|
|
|
1824
1847
|
// the platform host, and an unknown slug / SSRF / timeout throws a
|
|
1825
1848
|
// catchable Error. New entry in ACTION_SCRIPT_GLOBALS only — no widget
|
|
1826
1849
|
// hook, primitive, manifest field, or token changed shape; minor bump.
|
|
1827
|
-
|
|
1850
|
+
//
|
|
1851
|
+
// 1.38.0: additive (sc-2314) — new `usePdfExport({ spaceType, folderId? })`
|
|
1852
|
+
// hook. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to
|
|
1853
|
+
// a PDF server-side (the host's headless-Chromium pipeline) and SAVES it
|
|
1854
|
+
// into the Filestore via `ctx.filestore.files.exportPdf`, resolving to the
|
|
1855
|
+
// created file row (`application/pdf`). Reuses the filestore owner_id
|
|
1856
|
+
// resolution + per-folder write gate; gated on the existing
|
|
1857
|
+
// `files.write:*` scope. The same server-side renderer backs the web
|
|
1858
|
+
// Player and the native export, so no browser-only PDF library is added
|
|
1859
|
+
// to the vetted set. No existing hook, primitive, manifest field, or
|
|
1860
|
+
// token changed shape — minor bump.
|
|
1861
|
+
// 1.39.0: additive (sc-2327) — the `valueRef` propertySchema binding gains
|
|
1862
|
+
// an optional `mode` field: "static" (a pinned recordId, the default and
|
|
1863
|
+
// the only prior behaviour) or "latest" (the most recently created row,
|
|
1864
|
+
// resolved live by `created_at` desc with limit 1; recordId ignored). The
|
|
1865
|
+
// Field Value widget reads it to show a live "latest entry". Existing
|
|
1866
|
+
// bindings have no `mode` and read as static — additive, minor bump.
|
|
1867
|
+
version: "1.39.0",
|
|
1828
1868
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1829
1869
|
hooks: HOOKS,
|
|
1830
1870
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -235,6 +235,29 @@ const HOOKS = [
|
|
|
235
235
|
requiredContextSlice: ["filestore.files"],
|
|
236
236
|
scopes: ["files.write:*"],
|
|
237
237
|
},
|
|
238
|
+
{
|
|
239
|
+
name: "usePdfExport",
|
|
240
|
+
signature: "usePdfExport({ spaceType, folderId? })",
|
|
241
|
+
description:
|
|
242
|
+
"Render an HTML string to a PDF server-side and SAVE it as a file in " +
|
|
243
|
+
"the end-user's Filestore space. The widget passes the SPACE " +
|
|
244
|
+
"(`{ spaceType, folderId? }`); the hook resolves owner_id from the host " +
|
|
245
|
+
"context and POSTs JSON through ctx.filestore.files.exportPdf. " +
|
|
246
|
+
"`exportToPdf(html, { fileName?, folderId? })` resolves to the created " +
|
|
247
|
+
"file row (mime_type `application/pdf`) or throws the wire error; a 404 " +
|
|
248
|
+
"means the destination folder denied a write, a 413 that the rendered " +
|
|
249
|
+
"PDF exceeded the size cap. The HTML is rendered by the host's PDF " +
|
|
250
|
+
"service (headless Chromium) — the SAME server-side pipeline on the web " +
|
|
251
|
+
"Player and the native export, so no browser-only PDF library is used.",
|
|
252
|
+
returnShape: {
|
|
253
|
+
exportToPdf: "(html, { fileName?, folderId? }) => Promise<FilestoreFile>",
|
|
254
|
+
exporting: "boolean",
|
|
255
|
+
error: "Error | null",
|
|
256
|
+
lastExported: "FilestoreFile | null",
|
|
257
|
+
},
|
|
258
|
+
requiredContextSlice: ["filestore.files"],
|
|
259
|
+
scopes: ["files.write:*"],
|
|
260
|
+
},
|
|
238
261
|
{
|
|
239
262
|
name: "useFilestoreFolders",
|
|
240
263
|
signature: "useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })",
|
|
@@ -975,9 +998,9 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
975
998
|
},
|
|
976
999
|
user: {
|
|
977
1000
|
description:
|
|
978
|
-
"Signed-in user, host-
|
|
1001
|
+
"Signed-in user, host-built context object (camelCase: { id, email, displayName, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. Not a data-client.",
|
|
979
1002
|
required: true,
|
|
980
|
-
fields: { id: "string", email: "string",
|
|
1003
|
+
fields: { id: "string", email: "string", displayName: "string" },
|
|
981
1004
|
},
|
|
982
1005
|
workspace: {
|
|
983
1006
|
description:
|
|
@@ -1824,7 +1847,24 @@ const CONTRACT = deepFreeze({
|
|
|
1824
1847
|
// the platform host, and an unknown slug / SSRF / timeout throws a
|
|
1825
1848
|
// catchable Error. New entry in ACTION_SCRIPT_GLOBALS only — no widget
|
|
1826
1849
|
// hook, primitive, manifest field, or token changed shape; minor bump.
|
|
1827
|
-
|
|
1850
|
+
//
|
|
1851
|
+
// 1.38.0: additive (sc-2314) — new `usePdfExport({ spaceType, folderId? })`
|
|
1852
|
+
// hook. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to
|
|
1853
|
+
// a PDF server-side (the host's headless-Chromium pipeline) and SAVES it
|
|
1854
|
+
// into the Filestore via `ctx.filestore.files.exportPdf`, resolving to the
|
|
1855
|
+
// created file row (`application/pdf`). Reuses the filestore owner_id
|
|
1856
|
+
// resolution + per-folder write gate; gated on the existing
|
|
1857
|
+
// `files.write:*` scope. The same server-side renderer backs the web
|
|
1858
|
+
// Player and the native export, so no browser-only PDF library is added
|
|
1859
|
+
// to the vetted set. No existing hook, primitive, manifest field, or
|
|
1860
|
+
// token changed shape — minor bump.
|
|
1861
|
+
// 1.39.0: additive (sc-2327) — the `valueRef` propertySchema binding gains
|
|
1862
|
+
// an optional `mode` field: "static" (a pinned recordId, the default and
|
|
1863
|
+
// the only prior behaviour) or "latest" (the most recently created row,
|
|
1864
|
+
// resolved live by `created_at` desc with limit 1; recordId ignored). The
|
|
1865
|
+
// Field Value widget reads it to show a live "latest entry". Existing
|
|
1866
|
+
// bindings have no `mode` and read as static — additive, minor bump.
|
|
1867
|
+
version: "1.39.0",
|
|
1828
1868
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1829
1869
|
hooks: HOOKS,
|
|
1830
1870
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -113,7 +113,7 @@ export function useWidgetStyle() {
|
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* Returns the active end-user identity VERBATIM from the host, e.g.
|
|
116
|
-
* `{ id, email,
|
|
116
|
+
* `{ id, email, displayName, roles, groupIds }` (camelCase — host-built context, not a wire payload).
|
|
117
117
|
*
|
|
118
118
|
* `id` is `null` for anonymous visitors (and on the Studio canvas preview,
|
|
119
119
|
* which renders widgets as if signed-out so the public branch shows). The
|
|
@@ -1578,6 +1578,87 @@ export function useFilestoreUpload(options) {
|
|
|
1578
1578
|
return { upload, uploading, error, lastUploaded };
|
|
1579
1579
|
}
|
|
1580
1580
|
|
|
1581
|
+
/**
|
|
1582
|
+
* sc-2314 — render an HTML string to a PDF server-side and SAVE it into the
|
|
1583
|
+
* end-user's Filestore space. Returns `{ exportToPdf, exporting, error,
|
|
1584
|
+
* lastExported }`. The widget passes the SPACE (`{ spaceType, folderId? }`);
|
|
1585
|
+
* the hook resolves `owner_id` the same way the upload hook does (tenant for
|
|
1586
|
+
* PROJECT, app user for PERSONAL) and posts JSON to
|
|
1587
|
+
* `ctx.filestore.files.exportPdf`.
|
|
1588
|
+
*
|
|
1589
|
+
* `exportToPdf(html, { fileName?, folderId? })` resolves to the created file
|
|
1590
|
+
* record (`{ id, name, mime_type: "application/pdf", presigned_url, … }`).
|
|
1591
|
+
* `fileName` defaults to `document.pdf` and is forced to a `.pdf` suffix by the
|
|
1592
|
+
* backend. The HTML is rendered by the host's PDF service (headless Chromium)
|
|
1593
|
+
* — the SAME server-side pipeline backs the web Player and the native export,
|
|
1594
|
+
* so the capability behaves identically on both platforms (no browser-only PDF
|
|
1595
|
+
* library is involved).
|
|
1596
|
+
*
|
|
1597
|
+
* A 404 means the destination folder denied a write; a 413 means the rendered
|
|
1598
|
+
* PDF exceeded the size cap.
|
|
1599
|
+
*/
|
|
1600
|
+
export function usePdfExport(options) {
|
|
1601
|
+
const ctx = useWidgetContextOrThrow("usePdfExport");
|
|
1602
|
+
if (
|
|
1603
|
+
!ctx.filestore ||
|
|
1604
|
+
!ctx.filestore.files ||
|
|
1605
|
+
typeof ctx.filestore.files.exportPdf !== "function"
|
|
1606
|
+
) {
|
|
1607
|
+
throw new Error(
|
|
1608
|
+
"usePdfExport: host did not inject a filestore client (ctx.filestore.files.exportPdf)",
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
const { spaceType = "project", folderId: defaultFolderId = null } = options || {};
|
|
1612
|
+
const ownerId = _filestoreOwnerId(ctx, spaceType);
|
|
1613
|
+
|
|
1614
|
+
const [exporting, setExporting] = useState(false);
|
|
1615
|
+
const [error, setError] = useState(null);
|
|
1616
|
+
const [lastExported, setLastExported] = useState(null);
|
|
1617
|
+
|
|
1618
|
+
const filesRef = useRef(ctx.filestore.files);
|
|
1619
|
+
filesRef.current = ctx.filestore.files;
|
|
1620
|
+
|
|
1621
|
+
const exportToPdf = useCallback(
|
|
1622
|
+
async (html, overrides) => {
|
|
1623
|
+
if (typeof html !== "string" || html.trim().length === 0) {
|
|
1624
|
+
throw new Error("usePdfExport: html is required");
|
|
1625
|
+
}
|
|
1626
|
+
if (!ownerId) {
|
|
1627
|
+
const err = new Error("Sign in to save a PDF");
|
|
1628
|
+
setError(err);
|
|
1629
|
+
throw err;
|
|
1630
|
+
}
|
|
1631
|
+
const folderId =
|
|
1632
|
+
overrides && Object.prototype.hasOwnProperty.call(overrides, "folderId")
|
|
1633
|
+
? overrides.folderId
|
|
1634
|
+
: defaultFolderId;
|
|
1635
|
+
const fileName = overrides && overrides.fileName;
|
|
1636
|
+
const body = {
|
|
1637
|
+
html,
|
|
1638
|
+
space_type: String(spaceType || "project").toUpperCase(),
|
|
1639
|
+
owner_id: ownerId,
|
|
1640
|
+
};
|
|
1641
|
+
if (folderId) body.folder_id = folderId;
|
|
1642
|
+
if (fileName) body.file_name = fileName;
|
|
1643
|
+
setExporting(true);
|
|
1644
|
+
setError(null);
|
|
1645
|
+
try {
|
|
1646
|
+
const created = await filesRef.current.exportPdf(body);
|
|
1647
|
+
setLastExported(created || null);
|
|
1648
|
+
setExporting(false);
|
|
1649
|
+
return created;
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
setError(err);
|
|
1652
|
+
setExporting(false);
|
|
1653
|
+
throw err;
|
|
1654
|
+
}
|
|
1655
|
+
},
|
|
1656
|
+
[ownerId, defaultFolderId, spaceType],
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
return { exportToPdf, exporting, error, lastExported };
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1581
1662
|
/**
|
|
1582
1663
|
* Browse the end-user's Filestore folders. Returns { folders, loading, error,
|
|
1583
1664
|
* refetch }. Mirrors useFilestoreFiles for subfolder navigation: the widget
|
package/dist/index.d.ts
CHANGED
|
@@ -101,6 +101,12 @@ export interface ValueRefBinding {
|
|
|
101
101
|
tableId?: string;
|
|
102
102
|
recordId?: string;
|
|
103
103
|
column?: string;
|
|
104
|
+
/**
|
|
105
|
+
* How the record is chosen. "static" (default, may be omitted) pins
|
|
106
|
+
* `recordId`; "latest" resolves the most recently created row live and
|
|
107
|
+
* ignores `recordId`.
|
|
108
|
+
*/
|
|
109
|
+
mode?: "static" | "latest";
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
export interface WidgetEventDescriptor {
|
|
@@ -424,13 +430,13 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
424
430
|
* Absent / `false` everywhere the host has not opted the widget into filling.
|
|
425
431
|
*/
|
|
426
432
|
fill?: boolean;
|
|
427
|
-
/** Active end-user identity,
|
|
433
|
+
/** Active end-user identity from the host-built context (camelCase, not a wire payload). `id` is null when anonymous. */
|
|
428
434
|
user: {
|
|
429
435
|
id: string | null;
|
|
430
436
|
email: string | null;
|
|
431
|
-
|
|
437
|
+
displayName: string | null;
|
|
432
438
|
roles: string[];
|
|
433
|
-
|
|
439
|
+
groupIds: string[];
|
|
434
440
|
};
|
|
435
441
|
workspace: {
|
|
436
442
|
id: string;
|
|
@@ -883,9 +889,9 @@ export function useI18n(): {
|
|
|
883
889
|
export function useUser(): {
|
|
884
890
|
id: string | null;
|
|
885
891
|
email: string | null;
|
|
886
|
-
|
|
892
|
+
displayName: string | null;
|
|
887
893
|
roles: string[];
|
|
888
|
-
|
|
894
|
+
groupIds: string[];
|
|
889
895
|
};
|
|
890
896
|
|
|
891
897
|
/**
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/property-schema.js
CHANGED
|
@@ -183,10 +183,12 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
183
183
|
}
|
|
184
184
|
return value.map((item, i) => coerceLeaf(def.items, item, `${path}[${i}]`, errors));
|
|
185
185
|
case "valueRef": {
|
|
186
|
-
// REQ-WDG-VALUEREF: a `{ tableId, recordId, column }` binding.
|
|
187
|
-
// sub-field is
|
|
186
|
+
// REQ-WDG-VALUEREF: a `{ tableId, recordId, column, mode }` binding.
|
|
187
|
+
// Each string sub-field is optional (a half-configured binding is valid
|
|
188
188
|
// while the author is still picking); the bound widget treats any
|
|
189
|
-
// missing piece as "no value" and shows its fallback.
|
|
189
|
+
// missing piece as "no value" and shows its fallback. sc-2327: `mode`
|
|
190
|
+
// selects how the record is chosen — "static" (a pinned recordId, the
|
|
191
|
+
// default) or "latest" (the newest row, resolved live; recordId ignored).
|
|
190
192
|
if (!isPlainObject(value)) {
|
|
191
193
|
errors.push(`${path}: expected object`);
|
|
192
194
|
return value;
|
|
@@ -197,6 +199,14 @@ function coerceLeaf(def, value, path, errors) {
|
|
|
197
199
|
errors.push(`${path}.${sub}: expected string`);
|
|
198
200
|
}
|
|
199
201
|
}
|
|
202
|
+
if (
|
|
203
|
+
value.mode !== undefined &&
|
|
204
|
+
value.mode !== null &&
|
|
205
|
+
value.mode !== "static" &&
|
|
206
|
+
value.mode !== "latest"
|
|
207
|
+
) {
|
|
208
|
+
errors.push(`${path}.mode: expected "static" or "latest"`);
|
|
209
|
+
}
|
|
200
210
|
return value;
|
|
201
211
|
}
|
|
202
212
|
case "object": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|