@colixsystems/widget-sdk 0.267.0 → 0.268.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 CHANGED
@@ -50,7 +50,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
50
50
  | **CORE** | `useTranslate()` | `{ translate, translating, error, language, available }` | `ctx.i18n.translate` — no scope. Machine-translates **user-generated content** (record text, file names, API payloads) into the app user's language; `useI18n().t()` is still the answer for your own copy. `translate(str)` → `Promise<string>`, `translate(str[])` → `Promise<string[]>` in ONE request. Target defaults to the app user's language. Cached per session, per pod, and durably per workspace, so repeat text is free. Limits: 50 segments / 5 000 chars each / 20 000 total. Rejects with `TranslateError`; `available` is false where the host cannot translate. |
51
51
  | **CORE** | `useLinkPreview(url)` | `{ preview, loading, error, available }` — `preview` is `{ url, title, description, site_name, image_url } \| null` | `ctx.linkPreviews.get` — no scope. A link's title, description, site name and image, fetched by the platform (the device never contacts the linked site). A page with nothing to show resolves with null fields, not an error — render the plain link. A non-http(s) `url` makes no request. Memoized for up to an hour across widgets; concurrent calls for one URL share a request. `error` is a `LinkPreviewError` (`VALIDATION` \| `AUTH_REQUIRED` \| `RATE_LIMITED` \| `NETWORK` \| `INTERNAL`). `available` is false on a host with no link-preview client |
52
52
  | **CORE** | `useStableQuery(buildQuery)` | `T \| undefined` (whatever `buildQuery()` returns) | No context slice, no scope. Keeps `buildQuery()`'s result at a STABLE reference across renders when its (JSON-serialised) content hasn't changed, so `useDatastoreQuery(tableId, useStableQuery(() => ({...})))` replaces a hand-rolled `useMemo` with an easy-to-get-wrong deps array. Never throws: a `buildQuery` that itself throws degrades to a stable `undefined`; a result that can't be diffed (e.g. circular) degrades to "always a new reference". |
53
- | **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options)` (optional: options) | `{ data, total, loading, fetching, error, refetch }` — `loading` only until the first result for the table; a changed query / refetch keeps `data` while `fetching`; `total` is the envelope's `meta.total` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
53
+ | **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options)` (optional: options) | `{ data, total, loading, fetching, error, refetch }` — `loading` only until the first result for the table; a changed query / refetch keeps `data` while `fetching`; `total` is the envelope's `meta.total` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*`. `options` is `{ filter, filterMode, q, sort, limit, offset, includeCapabilities }`, applied on the server: see *Querying records*. |
54
54
  | **DATASTORE** | `useDatastoreFigure(table, { aggregate, column, filter })` (optional: options) | `{ value, text, truncated, loading, fetching, error, refetch }` | `records(table).list` (a count reads the envelope total; a sum or average up to 1000 rows) — `datastore.read:*`. One count / sum / average for a `<Stat>`, computed exactly as the built-in Stat widget does. |
55
55
  | **DATASTORE** | `useDatastoreRecord(table, id, options)` (optional: options) | `{ data, loading, fetching, error, refetch }` — a refetch of the same record keeps `data` while `fetching`; `{ includeCapabilities: true }` adds `data._capabilities` | `records(table).get` — `datastore.read:<table>` |
56
56
  | **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
@@ -86,7 +86,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
86
86
 
87
87
  ## Status
88
88
 
89
- `v0.267.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**.
89
+ `v0.268.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.268.0 (contract 1.218.0)
92
+
93
+ **`useAction().available` tells the truth about the Action.** It used to be true whenever an Action was bound, so a widget offered a run that could never happen. It now turns false once the server says a run would not reach the script: the Action is switched off or paused, not allowed from the app, gone, or the viewer is not a signed-in app user (signed-out visitors and anonymous accounts can't run Actions). It is asked again when the binding or the signed-in user changes, and stays true while no answer has come. The actions client (`@colixsystems/actions-client` 0.2.0) gains `status(actionId)` for it.
94
+
95
+ ### What's new in 0.267.1 (contract 1.217.0)
96
+
97
+ **The record query options are documented where an author reads them.** A new *Querying records* section lists everything `useDatastoreQuery` sends to the server — `filter` and its operators, `filterMode`, `q`, `sort`, `limit` (50 by default, at most 1000) and `offset` — with `total` for paging and a searchable, paged list to start from. The `Query` type now declares `q`, `offset`, `filterMode` and both filter and sort shapes, and marks `cursor`, which the datastore never read, deprecated. No runtime change.
90
98
 
91
99
  ### What's new in 0.267.0 (contract 1.217.0)
92
100
 
@@ -3543,6 +3551,64 @@ export default function OpenTasks({ tableId, titleField, statusField }) {
3543
3551
 
3544
3552
  `useCanWrite(tableId)` answers "may I create a row here". It says nothing about changing an EXISTING row: gate a list's Edit / Delete on each row's `_capabilities` (read with `includeCapabilities: true`, see *What's new in 0.174.0*), and a single-record page on `useCanWrite(tableId, { recordId })` or `useDatastoreRecord(…, { includeCapabilities: true })`. Never call `useCanWrite` with a `recordId` per list row — that is one request per row. Either answer is a FLOOR, not the whole rule: a widget whose own logic is more specific ("only the assignee may edit this row") still hand-checks that in addition (`user.id === record[assigneeField]`).
3545
3553
 
3554
+ ## Querying records
3555
+
3556
+ `useDatastoreQuery(tableId, query)` hands `query` to the datastore, which filters, searches, sorts and pages the table before a row reaches the widget. Every key is optional. Build the object with `useStableQuery` so it does not refetch every render.
3557
+
3558
+ | Option | What the server does with it |
3559
+ | --- | --- |
3560
+ | `filter` | Keeps the rows that match. A map `{ column: "op:value" }` (an array of expressions puts a range on one column), or an array `[{ column, operator, value }]`. |
3561
+ | `filterMode` | `"and"` (the default) keeps rows matching every condition; `"or"` keeps rows matching any. |
3562
+ | `q` | Free-text search: a case-insensitive substring match across every text column of the table (encrypted columns excepted). |
3563
+ | `sort` | `{ field, dir }` with `dir` `"asc"` or `"desc"`, or the string `"field:desc"`. One column; `created_at` and `updated_at` work too. |
3564
+ | `limit` | Rows per page: 50 when left out, at most 1000. |
3565
+ | `offset` | Rows to skip, for the next page. |
3566
+ | `includeCapabilities` | `true` stamps each row with `_capabilities`, what the signed-in user may do to it. |
3567
+
3568
+ The filter operators are `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `contains` (a case-insensitive substring), `empty` and `nempty` (no value). In the array form a condition may carry `valueMode`: `"currentUser"` or `"currentUserGroup"` with `eq` or `neq` matches the signed-in user (or their groups) on a USER or USER_GROUP column, and `"relativeDate"` with an ordering operator reads `value` as "that many days ago".
3569
+
3570
+ The result's `total` is how many rows match in all, not just this page: show "12 of 312" and page with `offset`. For a count, a sum or an average, use `useDatastoreFigure(tableId, { aggregate, column, filter })` instead of reading rows.
3571
+
3572
+ **Narrow on the server, never over a fetched batch.** A widget that fetches `limit: 1000` and then filters or searches the rows in JavaScript only ever sees the first 1000; once the table grows past that, it misses rows without saying so. Send the search box's text as `q`, a chip's condition as `filter`, and page with `offset`. The checker warns about the batch pattern.
3573
+
3574
+ ```js
3575
+ import { useEffect, useState } from "react";
3576
+ import { Pressable, Text, TextInput, View, useDatastoreQuery, useStableQuery } from "@colixsystems/widget-sdk";
3577
+
3578
+ const PAGE = 50;
3579
+
3580
+ export default function ContactList({ tableId }) {
3581
+ const [draft, setDraft] = useState("");
3582
+ const [search, setSearch] = useState("");
3583
+ const [status, setStatus] = useState("");
3584
+ const [page, setPage] = useState(0);
3585
+ useEffect(() => {
3586
+ const wait = setTimeout(() => { setSearch(draft.trim()); setPage(0); }, 250);
3587
+ return () => clearTimeout(wait);
3588
+ }, [draft]);
3589
+ const query = useStableQuery(() => ({
3590
+ q: search,
3591
+ filter: status ? { status: `eq:${status}` } : undefined,
3592
+ sort: { field: "last_contacted", dir: "desc" },
3593
+ limit: PAGE,
3594
+ offset: page * PAGE,
3595
+ }));
3596
+ const { data, total, loading } = useDatastoreQuery(tableId, query);
3597
+ const pick = (next) => { setStatus(next); setPage(0); };
3598
+ return (
3599
+ <View>
3600
+ <TextInput value={draft} onChangeText={setDraft} placeholder="Search" />
3601
+ <Pressable onPress={() => pick(status === "Won" ? "" : "Won")}><Text>Won</Text></Pressable>
3602
+ {loading ? <Text>Loading…</Text> : data.map((row) => <Text key={row.id}>{row.name}</Text>)}
3603
+ <Text>{total ?? 0} contacts</Text>
3604
+ {(page + 1) * PAGE < (total ?? 0) ? (
3605
+ <Pressable onPress={() => setPage(page + 1)}><Text>Next page</Text></Pressable>
3606
+ ) : null}
3607
+ </View>
3608
+ );
3609
+ }
3610
+ ```
3611
+
3546
3612
  ## Cross-platform widgets (single-file vs split)
3547
3613
 
3548
3614
  Every widget runs in **both** the web Player and the exported native (Expo) app.
package/dist/contract.cjs CHANGED
@@ -2184,7 +2184,12 @@ const HOOKS = [
2184
2184
  "`actionRef` is an Action id an author bound through an `actionRef` " +
2185
2185
  "property — when it is missing, or the host brokers no actions client, " +
2186
2186
  "`available` is false and run() rejects UNAVAILABLE; render a visible " +
2187
- "'pick an action' state, NEVER a blank box. A changed binding clears " +
2187
+ "'pick an action' state, NEVER a blank box. sc-14224: `available` also " +
2188
+ "turns false once the server says a run would not reach the script — " +
2189
+ "the Action is switched off or paused, not allowed from the app, gone, " +
2190
+ "or the viewer is not a signed-in app user (signed-out visitors and " +
2191
+ "anonymous accounts can't run Actions) — so render an 'isn't available' " +
2192
+ "state then too. It stays true while no answer has come. A changed binding clears " +
2188
2193
  "`result` and `error`, and a run that settles after the binding changed " +
2189
2194
  "(or after unmount) writes nothing. ActionError.code: " +
2190
2195
  "INVALID_ACTION_INPUT (400) — the input was refused; " +
@@ -2195,7 +2200,7 @@ const HOOKS = [
2195
2200
  "MARKDOWN, so render it with <RichText value={result} />, not <Text>. " +
2196
2201
  "Reads ctx.actions.",
2197
2202
  returnShape: {
2198
- available: "boolean // false when no Action is bound, or the host brokers none",
2203
+ available: "boolean // false when no Action is bound, the host brokers none, or the server says a run would not reach the script",
2199
2204
  run: "(input) => Promise<unknown> // input is optional; resolves with the Action's respond() value, or null; rejects with ActionError",
2200
2205
  running: "boolean // true while any run is in flight",
2201
2206
  result: "unknown // the last successful run's result; null before the first run",
@@ -4150,13 +4155,14 @@ const WIDGET_CONTEXT_SHAPE = {
4150
4155
  actions: {
4151
4156
  description:
4152
4157
  "sc-13843 — injected @colixsystems/actions-client instance. " +
4153
- "{ run(actionId, input?) -> Promise<{ ok, result }> }. Backs useAction(). " +
4158
+ "{ run(actionId, input?) -> Promise<{ ok, result }>, status(actionId) -> " +
4159
+ "Promise<{ available, reason }> }. Backs useAction(). " +
4154
4160
  "Runs one Action as the signed-in caller; the server refuses an Action " +
4155
4161
  "without the app trigger. Errors carry the server's `code` and `status`. " +
4156
4162
  "Absent on a host that brokers no actions client, where useAction() " +
4157
4163
  "degrades to available: false rather than throwing.",
4158
4164
  required: false,
4159
- fields: { run: "function" },
4165
+ fields: { run: "function", status: "function" },
4160
4166
  },
4161
4167
  linkPreviews: {
4162
4168
  description:
@@ -6472,7 +6478,10 @@ const CONTRACT = deepFreeze({
6472
6478
  // 1.217.0: additive (sc-4747) — `TextInput` documents `keyboardType`,
6473
6479
  // `autoCapitalize`, `autoCorrect`, `autoComplete`, `secureTextEntry`;
6474
6480
  // `useDatastoreSchema` columns document `text_format` (snake_case wire).
6475
- version: "1.217.0",
6481
+ // 1.218.0: additive (sc-14224) — the actions client's `status(actionId)`;
6482
+ // `useAction().available` turns false when the server says a run would
6483
+ // not reach the script.
6484
+ version: "1.218.0",
6476
6485
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6477
6486
  hooks: HOOKS,
6478
6487
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -2181,7 +2181,12 @@ const HOOKS = [
2181
2181
  "`actionRef` is an Action id an author bound through an `actionRef` " +
2182
2182
  "property — when it is missing, or the host brokers no actions client, " +
2183
2183
  "`available` is false and run() rejects UNAVAILABLE; render a visible " +
2184
- "'pick an action' state, NEVER a blank box. A changed binding clears " +
2184
+ "'pick an action' state, NEVER a blank box. sc-14224: `available` also " +
2185
+ "turns false once the server says a run would not reach the script — " +
2186
+ "the Action is switched off or paused, not allowed from the app, gone, " +
2187
+ "or the viewer is not a signed-in app user (signed-out visitors and " +
2188
+ "anonymous accounts can't run Actions) — so render an 'isn't available' " +
2189
+ "state then too. It stays true while no answer has come. A changed binding clears " +
2185
2190
  "`result` and `error`, and a run that settles after the binding changed " +
2186
2191
  "(or after unmount) writes nothing. ActionError.code: " +
2187
2192
  "INVALID_ACTION_INPUT (400) — the input was refused; " +
@@ -2192,7 +2197,7 @@ const HOOKS = [
2192
2197
  "MARKDOWN, so render it with <RichText value={result} />, not <Text>. " +
2193
2198
  "Reads ctx.actions.",
2194
2199
  returnShape: {
2195
- available: "boolean // false when no Action is bound, or the host brokers none",
2200
+ available: "boolean // false when no Action is bound, the host brokers none, or the server says a run would not reach the script",
2196
2201
  run: "(input) => Promise<unknown> // input is optional; resolves with the Action's respond() value, or null; rejects with ActionError",
2197
2202
  running: "boolean // true while any run is in flight",
2198
2203
  result: "unknown // the last successful run's result; null before the first run",
@@ -4147,13 +4152,14 @@ const WIDGET_CONTEXT_SHAPE = {
4147
4152
  actions: {
4148
4153
  description:
4149
4154
  "sc-13843 — injected @colixsystems/actions-client instance. " +
4150
- "{ run(actionId, input?) -> Promise<{ ok, result }> }. Backs useAction(). " +
4155
+ "{ run(actionId, input?) -> Promise<{ ok, result }>, status(actionId) -> " +
4156
+ "Promise<{ available, reason }> }. Backs useAction(). " +
4151
4157
  "Runs one Action as the signed-in caller; the server refuses an Action " +
4152
4158
  "without the app trigger. Errors carry the server's `code` and `status`. " +
4153
4159
  "Absent on a host that brokers no actions client, where useAction() " +
4154
4160
  "degrades to available: false rather than throwing.",
4155
4161
  required: false,
4156
- fields: { run: "function" },
4162
+ fields: { run: "function", status: "function" },
4157
4163
  },
4158
4164
  linkPreviews: {
4159
4165
  description:
@@ -6469,7 +6475,10 @@ const CONTRACT = deepFreeze({
6469
6475
  // 1.217.0: additive (sc-4747) — `TextInput` documents `keyboardType`,
6470
6476
  // `autoCapitalize`, `autoCorrect`, `autoComplete`, `secureTextEntry`;
6471
6477
  // `useDatastoreSchema` columns document `text_format` (snake_case wire).
6472
- version: "1.217.0",
6478
+ // 1.218.0: additive (sc-14224) — the actions client's `status(actionId)`;
6479
+ // `useAction().available` turns false when the server says a run would
6480
+ // not reach the script.
6481
+ version: "1.218.0",
6473
6482
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6474
6483
  hooks: HOOKS,
6475
6484
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -3893,18 +3893,46 @@ function toActionError(err) {
3893
3893
  * `available` is false and `run` rejects UNAVAILABLE rather than throwing at
3894
3894
  * render — the widget must render a visible unbound state (CLAUDE.md §8).
3895
3895
  *
3896
+ * sc-14224 — `available` also turns false once the server says a run would
3897
+ * not reach the script: the Action is switched off, paused, not allowed from
3898
+ * the app, gone, or the viewer is not a signed-in app user. It is asked again
3899
+ * when the binding or the signed-in user changes. With no answer (offline, an
3900
+ * older host) it stays true and `run` reports the refusal.
3901
+ *
3896
3902
  * Routes through the injected `@colixsystems/actions-client` at `ctx.actions`,
3897
3903
  * so the Player and the Expo export resolve against the identical client.
3898
3904
  */
3899
3905
  export function useAction(actionRef) {
3900
3906
  const ctx = useWidgetContextOrThrow("useAction");
3901
- const available =
3907
+ const bound =
3902
3908
  Boolean(actionRef) &&
3903
3909
  Boolean(ctx.actions) &&
3904
3910
  typeof ctx.actions.run === "function";
3911
+ const canAsk = bound && typeof ctx.actions.status === "function";
3912
+ const askedAs = `${actionRef}|${(ctx.user && ctx.user.id) || ""}`;
3905
3913
 
3906
3914
  const clientRef = useRef(null);
3907
- clientRef.current = available ? ctx.actions : null;
3915
+ clientRef.current = bound ? ctx.actions : null;
3916
+
3917
+ // The binding and viewer the server last refused, so a rebind starts fresh.
3918
+ const [refusedAs, setRefusedAs] = useState(null);
3919
+ useEffect(() => {
3920
+ const client = clientRef.current;
3921
+ if (!canAsk || !client) return undefined;
3922
+ let live = true;
3923
+ client.status(actionRef).then(
3924
+ (answer) => {
3925
+ if (live) setRefusedAs(answer && answer.available === false ? askedAs : null);
3926
+ },
3927
+ // No answer is not a refusal: keep offering the run.
3928
+ () => {},
3929
+ );
3930
+ return () => {
3931
+ live = false;
3932
+ };
3933
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3934
+ }, [askedAs, canAsk]);
3935
+ const available = bound && refusedAs !== askedAs;
3908
3936
  const actionIdRef = useRef(actionRef);
3909
3937
  actionIdRef.current = actionRef;
3910
3938
 
package/dist/index.d.ts CHANGED
@@ -890,6 +890,8 @@ export interface WidgetContext<TProps = unknown> {
890
890
  /** sc-13843 — injected @colixsystems/actions-client backing useAction(). */
891
891
  actions?: {
892
892
  run(actionId: string, input?: Record<string, unknown> | null): Promise<{ ok: boolean; result: unknown }>;
893
+ /** sc-14224 — whether a run would reach the script; optional on older hosts. */
894
+ status?(actionId: string): Promise<{ available: boolean; reason: string | null }>;
893
895
  };
894
896
  /** Injected @colixsystems/link-preview-client backing useLinkPreview(). */
895
897
  linkPreviews?: { get(url: string): Promise<LinkPreview> };
@@ -1074,10 +1076,23 @@ export function hasCornerRadius(
1074
1076
  radius: ResolvedCornerRadius | null | undefined,
1075
1077
  ): boolean;
1076
1078
 
1079
+ /** A record query the datastore applies on the server; see the README's "Querying records". */
1077
1080
  export interface Query {
1078
- filter?: Record<string, unknown>;
1079
- sort?: Array<{ field: string; dir: "asc" | "desc" }>;
1081
+ /** `{ column: "op:value" }` (an array of expressions for a range), or `[{ column, operator, value, valueMode }]`. */
1082
+ filter?:
1083
+ | Record<string, string | string[]>
1084
+ | Array<{ column: string; operator?: string; value?: unknown; valueMode?: string }>;
1085
+ /** `"and"` (the default) keeps rows matching every condition, `"or"` rows matching any. */
1086
+ filterMode?: "and" | "or";
1087
+ /** A case-insensitive substring search across the table's text columns. */
1088
+ q?: string;
1089
+ /** `{ field, dir }` or `"field:desc"`; one column, so an array uses its first entry. */
1090
+ sort?: string | { field: string; dir?: "asc" | "desc" } | Array<{ field: string; dir?: "asc" | "desc" }>;
1091
+ /** Rows per page: 50 when omitted, at most 1000. */
1080
1092
  limit?: number;
1093
+ /** Rows to skip, for the next page. */
1094
+ offset?: number;
1095
+ /** @deprecated The datastore ignores it; page with `offset`. */
1081
1096
  cursor?: string;
1082
1097
  /**
1083
1098
  * Stamp each row with `_capabilities` — what the signed-in user may do to
@@ -1880,7 +1895,11 @@ export interface AgentApi {
1880
1895
  export function useAgent(agentRef: string | null | undefined): AgentApi;
1881
1896
 
1882
1897
  export interface ActionApi {
1883
- /** False when no Action is bound, or the host brokers no actions client. */
1898
+ /**
1899
+ * False when no Action is bound, the host brokers no actions client, or the
1900
+ * server says a run would not reach the script (switched off, not allowed
1901
+ * from the app, or the viewer is not a signed-in app user).
1902
+ */
1884
1903
  available: boolean;
1885
1904
  /**
1886
1905
  * Imperative — call from an event handler, never on mount. Resolves with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.267.0",
3
+ "version": "0.268.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
@@ -2181,7 +2181,12 @@ const HOOKS = [
2181
2181
  "`actionRef` is an Action id an author bound through an `actionRef` " +
2182
2182
  "property — when it is missing, or the host brokers no actions client, " +
2183
2183
  "`available` is false and run() rejects UNAVAILABLE; render a visible " +
2184
- "'pick an action' state, NEVER a blank box. A changed binding clears " +
2184
+ "'pick an action' state, NEVER a blank box. sc-14224: `available` also " +
2185
+ "turns false once the server says a run would not reach the script — " +
2186
+ "the Action is switched off or paused, not allowed from the app, gone, " +
2187
+ "or the viewer is not a signed-in app user (signed-out visitors and " +
2188
+ "anonymous accounts can't run Actions) — so render an 'isn't available' " +
2189
+ "state then too. It stays true while no answer has come. A changed binding clears " +
2185
2190
  "`result` and `error`, and a run that settles after the binding changed " +
2186
2191
  "(or after unmount) writes nothing. ActionError.code: " +
2187
2192
  "INVALID_ACTION_INPUT (400) — the input was refused; " +
@@ -2192,7 +2197,7 @@ const HOOKS = [
2192
2197
  "MARKDOWN, so render it with <RichText value={result} />, not <Text>. " +
2193
2198
  "Reads ctx.actions.",
2194
2199
  returnShape: {
2195
- available: "boolean // false when no Action is bound, or the host brokers none",
2200
+ available: "boolean // false when no Action is bound, the host brokers none, or the server says a run would not reach the script",
2196
2201
  run: "(input) => Promise<unknown> // input is optional; resolves with the Action's respond() value, or null; rejects with ActionError",
2197
2202
  running: "boolean // true while any run is in flight",
2198
2203
  result: "unknown // the last successful run's result; null before the first run",
@@ -4147,13 +4152,14 @@ const WIDGET_CONTEXT_SHAPE = {
4147
4152
  actions: {
4148
4153
  description:
4149
4154
  "sc-13843 — injected @colixsystems/actions-client instance. " +
4150
- "{ run(actionId, input?) -> Promise<{ ok, result }> }. Backs useAction(). " +
4155
+ "{ run(actionId, input?) -> Promise<{ ok, result }>, status(actionId) -> " +
4156
+ "Promise<{ available, reason }> }. Backs useAction(). " +
4151
4157
  "Runs one Action as the signed-in caller; the server refuses an Action " +
4152
4158
  "without the app trigger. Errors carry the server's `code` and `status`. " +
4153
4159
  "Absent on a host that brokers no actions client, where useAction() " +
4154
4160
  "degrades to available: false rather than throwing.",
4155
4161
  required: false,
4156
- fields: { run: "function" },
4162
+ fields: { run: "function", status: "function" },
4157
4163
  },
4158
4164
  linkPreviews: {
4159
4165
  description:
@@ -6469,7 +6475,10 @@ const CONTRACT = deepFreeze({
6469
6475
  // 1.217.0: additive (sc-4747) — `TextInput` documents `keyboardType`,
6470
6476
  // `autoCapitalize`, `autoCorrect`, `autoComplete`, `secureTextEntry`;
6471
6477
  // `useDatastoreSchema` columns document `text_format` (snake_case wire).
6472
- version: "1.217.0",
6478
+ // 1.218.0: additive (sc-14224) — the actions client's `status(actionId)`;
6479
+ // `useAction().available` turns false when the server says a run would
6480
+ // not reach the script.
6481
+ version: "1.218.0",
6473
6482
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6474
6483
  hooks: HOOKS,
6475
6484
  primitives: PRIMITIVES,