@colixsystems/widget-sdk 0.40.2 → 0.42.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
@@ -13,7 +13,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
13
13
 
14
14
  **Wire / casing: snake_case end to end.** The clients send and return snake_case **verbatim** (`created_at`, `group_ids`, `can_read`, `amount_cents`, `data_type`, `is_active`, …). There is **no case transform anywhere** — not on the client and not in the backend; the only casing boundary is Prisma `@map` (snake_case field → camelCase column). Author-defined record column values pass through verbatim. Every `list(...)` returns the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you.
15
15
 
16
- **Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (18 hooks), grouped by the domain client each one reads. **CORE** hooks read host state directly off `WidgetContext` (no data client); the rest delegate to one of the four injected clients. The grouping mirrors the banner sections in [`src/hooks.js`](src/hooks.js).
16
+ **Hooks read the injected clients** — they do not hold their own HTTP. This is the **complete** hook surface (19 hooks), grouped by the domain client each one reads. **CORE** hooks read host state directly off `WidgetContext` (no data client); the rest delegate to one of the four injected clients. The grouping mirrors the banner sections in [`src/hooks.js`](src/hooks.js).
17
17
 
18
18
  | Group | Hook (signature) | Returns | Reads / scope |
19
19
  | ----- | ---------------- | ------- | ------------- |
@@ -24,6 +24,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
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) |
26
26
  | **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill its page-grid tile's reserved height (containers + media fill by default; the author can override per tile). Media-style widgets switch to a `flex: 1` / `height: "100%"` layout; others ignore it. Defaults `false`. |
27
+ | **CORE** | `useRefresh(handler)` | `void` | `ctx.refresh.subscribe` — no scope. Subscribes the handler to the page-level refresh tick (pull-to-refresh on mobile). Handler may return a Promise — the host waits for `allSettled` before clearing the spinner. The three datastore hooks auto-subscribe their own `refetch`; widgets only call this directly to re-run non-datastore work. No-op on a host that doesn't implement refresh. |
27
28
  | **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
28
29
  | **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
29
30
  | **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
@@ -47,7 +48,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
47
48
 
48
49
  ## Status
49
50
 
50
- `v0.40.2` — 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**.
51
+ `v0.42.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**.
52
+
53
+ ### What's new in 0.42.0
54
+
55
+ **RELATION columns hydrate with a display label (sc-1181).** Record reads now return `{ id, label }` for ONE_TO_ONE / ONE_TO_MANY and `[{ id, label }, ...]` for MANY_TO_MANY (empty array when no links) — `label` is the value of the column pointed at by the new optional `display_column_id` on `DatastoreSchemaColumn`, or, when unset, the first STRING/TEXT column on the target table. Widgets should render `record.<rel>.label` (or `record.<rel>.map(r => r.label).join(", ")` for M:M) directly; `.id` is still there for the foreign-key case. The cell-formatting helpers in the built-in `DataList`, `TabbedDatalist`, and `FieldValue` widgets already walk arrays and prefer `label` over `name` / `id` — author widgets that need the same can copy that pattern. `CONTRACT.version` is unchanged.
56
+
57
+ ### What's new in 0.41.0
58
+
59
+ **New `useRefresh(handler)` hook + page-level refresh signal (sc-1179).** Pull-to-refresh on the mobile web Player + the native Expo export's `RefreshControl` now fans a page-level refresh tick out to every widget on the page. The three datastore hooks — `useDatastoreQuery`, `useDatastoreRecord`, `useAsset` — auto-subscribe their own `refetch`, so a widget built on those hooks gets refreshed for free. Widgets that need to re-run other work (a third-party `fetch`, a derived calculation) call `useRefresh(async () => { … })` directly. The handler may return a Promise — the host waits on `Promise.allSettled` of every subscriber before clearing the spinner. The slot (`ctx.refresh.subscribe`) is optional on the WidgetContext: a host that does not implement refresh (the Studio canvas preview) simply omits it and the hook collapses to a no-op. Additive — `CONTRACT.version` bumped to the next minor since the contract grew a new hook + a new (optional) context slice.
51
60
 
52
61
  ### What's new in 0.40.2
53
62
 
package/dist/contract.cjs CHANGED
@@ -116,6 +116,26 @@ const HOOKS = [
116
116
  requiredContextSlice: ["renderer.renderNode"],
117
117
  scopes: null,
118
118
  },
119
+ {
120
+ name: "useRefresh",
121
+ signature: "useRefresh(handler)",
122
+ description:
123
+ "Subscribe to the page-level refresh tick (pull-to-refresh on " +
124
+ "mobile, manual refresh button, etc.). The handler is called every " +
125
+ "time the host triggers a refresh; it may return a Promise — the " +
126
+ "host waits on all settled handlers before clearing the refresh " +
127
+ "indicator. The three datastore hooks (useDatastoreQuery, " +
128
+ "useDatastoreRecord, useAsset) already subscribe automatically — " +
129
+ "widgets only call useRefresh directly when they need to re-run " +
130
+ "non-datastore work (a third-party fetch, a derived calculation). " +
131
+ "Safe to call on a host that does not implement refresh — it is a " +
132
+ "no-op there.",
133
+ returnShape: {
134
+ "(returns)": "void",
135
+ },
136
+ requiredContextSlice: ["refresh.subscribe"],
137
+ scopes: null,
138
+ },
119
139
  {
120
140
  name: "useNavigation",
121
141
  signature: "useNavigation()",
@@ -924,6 +944,21 @@ const WIDGET_CONTEXT_SHAPE = {
924
944
  required: true,
925
945
  fields: { renderNode: "function" },
926
946
  },
947
+ // sc-1179 — page-level refresh signal. Host owns the trigger (pull-to-
948
+ // refresh on mobile, refresh button, etc.); widgets opt in via
949
+ // useRefresh(handler). The three datastore hooks auto-subscribe; other
950
+ // widgets subscribe when they need to re-run non-datastore work. Optional
951
+ // so a host that does not implement refresh can simply omit it — the hook
952
+ // collapses to a no-op there.
953
+ refresh: {
954
+ description:
955
+ "Optional page-level refresh slot. { subscribe(handler) -> unsubscribe }. " +
956
+ "Handler is called on every page refresh tick (pull-to-refresh on " +
957
+ "mobile, manual refresh button); may return a Promise the host " +
958
+ "awaits before clearing the refresh indicator. Backs useRefresh().",
959
+ required: false,
960
+ fields: { subscribe: "function" },
961
+ },
927
962
  events: {
928
963
  description: "{ emit(name, payload) }.",
929
964
  required: true,
@@ -1582,7 +1617,17 @@ const CONTRACT = deepFreeze({
1582
1617
  // manual key entry, and a `widget.<id>.<key>` override still wins per
1583
1618
  // instance. No existing hook, primitive, manifest field, or token changed
1584
1619
  // shape — minor bump on the pre-1.0 channel.
1585
- version: "1.28.0",
1620
+ //
1621
+ // 1.29.0: additive (sc-1179) — page-level refresh signal. New hook
1622
+ // `useRefresh(handler)` and new optional WidgetContext slice
1623
+ // `refresh: { subscribe(handler) -> unsubscribe }`. The web Player's
1624
+ // pull-to-refresh gesture and the native Expo export's `RefreshControl`
1625
+ // both call into the same manager so a refresh tick fans out to every
1626
+ // widget on the page; the three datastore hooks (useDatastoreQuery /
1627
+ // useDatastoreRecord / useAsset) auto-subscribe their own `refetch`.
1628
+ // No existing hook, primitive, manifest field, or token changed shape
1629
+ // — minor bump on the pre-1.0 channel.
1630
+ version: "1.29.0",
1586
1631
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1587
1632
  hooks: HOOKS,
1588
1633
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -116,6 +116,26 @@ const HOOKS = [
116
116
  requiredContextSlice: ["renderer.renderNode"],
117
117
  scopes: null,
118
118
  },
119
+ {
120
+ name: "useRefresh",
121
+ signature: "useRefresh(handler)",
122
+ description:
123
+ "Subscribe to the page-level refresh tick (pull-to-refresh on " +
124
+ "mobile, manual refresh button, etc.). The handler is called every " +
125
+ "time the host triggers a refresh; it may return a Promise — the " +
126
+ "host waits on all settled handlers before clearing the refresh " +
127
+ "indicator. The three datastore hooks (useDatastoreQuery, " +
128
+ "useDatastoreRecord, useAsset) already subscribe automatically — " +
129
+ "widgets only call useRefresh directly when they need to re-run " +
130
+ "non-datastore work (a third-party fetch, a derived calculation). " +
131
+ "Safe to call on a host that does not implement refresh — it is a " +
132
+ "no-op there.",
133
+ returnShape: {
134
+ "(returns)": "void",
135
+ },
136
+ requiredContextSlice: ["refresh.subscribe"],
137
+ scopes: null,
138
+ },
119
139
  {
120
140
  name: "useNavigation",
121
141
  signature: "useNavigation()",
@@ -924,6 +944,21 @@ const WIDGET_CONTEXT_SHAPE = {
924
944
  required: true,
925
945
  fields: { renderNode: "function" },
926
946
  },
947
+ // sc-1179 — page-level refresh signal. Host owns the trigger (pull-to-
948
+ // refresh on mobile, refresh button, etc.); widgets opt in via
949
+ // useRefresh(handler). The three datastore hooks auto-subscribe; other
950
+ // widgets subscribe when they need to re-run non-datastore work. Optional
951
+ // so a host that does not implement refresh can simply omit it — the hook
952
+ // collapses to a no-op there.
953
+ refresh: {
954
+ description:
955
+ "Optional page-level refresh slot. { subscribe(handler) -> unsubscribe }. " +
956
+ "Handler is called on every page refresh tick (pull-to-refresh on " +
957
+ "mobile, manual refresh button); may return a Promise the host " +
958
+ "awaits before clearing the refresh indicator. Backs useRefresh().",
959
+ required: false,
960
+ fields: { subscribe: "function" },
961
+ },
927
962
  events: {
928
963
  description: "{ emit(name, payload) }.",
929
964
  required: true,
@@ -1582,7 +1617,17 @@ const CONTRACT = deepFreeze({
1582
1617
  // manual key entry, and a `widget.<id>.<key>` override still wins per
1583
1618
  // instance. No existing hook, primitive, manifest field, or token changed
1584
1619
  // shape — minor bump on the pre-1.0 channel.
1585
- version: "1.28.0",
1620
+ //
1621
+ // 1.29.0: additive (sc-1179) — page-level refresh signal. New hook
1622
+ // `useRefresh(handler)` and new optional WidgetContext slice
1623
+ // `refresh: { subscribe(handler) -> unsubscribe }`. The web Player's
1624
+ // pull-to-refresh gesture and the native Expo export's `RefreshControl`
1625
+ // both call into the same manager so a refresh tick fans out to every
1626
+ // widget on the page; the three datastore hooks (useDatastoreQuery /
1627
+ // useDatastoreRecord / useAsset) auto-subscribe their own `refetch`.
1628
+ // No existing hook, primitive, manifest field, or token changed shape
1629
+ // — minor bump on the pre-1.0 channel.
1630
+ version: "1.29.0",
1586
1631
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1587
1632
  hooks: HOOKS,
1588
1633
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -188,6 +188,49 @@ export function WidgetTree({ node }) {
188
188
  return ctx.renderer.renderNode(node);
189
189
  }
190
190
 
191
+ /**
192
+ * sc-1179 — subscribe to the page-level refresh tick.
193
+ *
194
+ * Pull-to-refresh on mobile (web Player at touch widths + the native Expo
195
+ * export's `RefreshControl`) triggers a page-wide refresh. Widgets that
196
+ * need to re-run work on a refresh — re-fetch from a third-party API,
197
+ * recompute a derived value — pass a handler here:
198
+ *
199
+ * useRefresh(async () => { await refetchMyThirdPartyData(); });
200
+ *
201
+ * The handler may return a Promise — the host waits on Promise.allSettled
202
+ * of every subscriber before clearing the refresh indicator. Handler
203
+ * identity does not need to be stable: the subscription captures it in a
204
+ * ref and reads the latest on every tick.
205
+ *
206
+ * `useDatastoreQuery` / `useDatastoreRecord` / `useAsset` already
207
+ * auto-subscribe their own `refetch`, so a widget built on the data hooks
208
+ * gets refreshed for free — call `useRefresh` directly only for
209
+ * non-datastore work.
210
+ *
211
+ * Safe to call on a host that does not implement refresh — the hook
212
+ * collapses to a no-op there.
213
+ */
214
+ export function useRefresh(handler) {
215
+ const ctx = useWidgetContextOrThrow("useRefresh");
216
+ const handlerRef = useRef(handler);
217
+ handlerRef.current = handler;
218
+ const subscribe = ctx.refresh && ctx.refresh.subscribe;
219
+ const subscribeRef = useRef(subscribe);
220
+ subscribeRef.current = subscribe;
221
+ useEffect(() => {
222
+ const sub = subscribeRef.current;
223
+ if (typeof sub !== "function") return undefined;
224
+ const unsubscribe = sub(() => {
225
+ const fn = handlerRef.current;
226
+ if (typeof fn === "function") return fn();
227
+ return undefined;
228
+ });
229
+ return typeof unsubscribe === "function" ? unsubscribe : undefined;
230
+ // eslint-disable-next-line react-hooks/exhaustive-deps
231
+ }, []);
232
+ }
233
+
191
234
  /**
192
235
  * Returns the host-provided navigation surface:
193
236
  * `{ goTo, goBack, push, replace, back, currentRoute }`.
@@ -478,6 +521,10 @@ export function useDatastoreQuery(table, query) {
478
521
  await doFetch();
479
522
  }, [doFetch]);
480
523
 
524
+ // sc-1179 — auto-subscribe to the page-level refresh tick so a
525
+ // pull-to-refresh on mobile re-runs the query without per-widget code.
526
+ useRefresh(refetch);
527
+
481
528
  return { data, loading, error, refetch };
482
529
  }
483
530
 
@@ -556,6 +603,9 @@ export function useDatastoreRecord(table, recordId) {
556
603
  await doFetch();
557
604
  }, [doFetch]);
558
605
 
606
+ // sc-1179 — auto-subscribe to the page-level refresh tick.
607
+ useRefresh(refetch);
608
+
559
609
  return { data, loading, error, refetch };
560
610
  }
561
611
 
@@ -1137,6 +1187,9 @@ export function useAsset(assetId) {
1137
1187
  await doFetch();
1138
1188
  }, [doFetch]);
1139
1189
 
1190
+ // sc-1179 — auto-subscribe to the page-level refresh tick.
1191
+ useRefresh(refetch);
1192
+
1140
1193
  const url =
1141
1194
  asset && typeof asset.url === "string" && asset.url.length > 0
1142
1195
  ? asset.url
package/dist/index.d.ts CHANGED
@@ -662,6 +662,13 @@ export interface DatastoreSchemaColumn {
662
662
  relation_type?: "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_MANY" | null;
663
663
  /** For RELATION columns only — the id of the table this column points at. */
664
664
  target_table_id?: string | null;
665
+ /**
666
+ * For RELATION columns only — id of a column on `target_table_id` whose
667
+ * value supplies the `label` hydrated next to the related record id in
668
+ * record responses. `null` falls back to the first STRING/TEXT column on
669
+ * the target table.
670
+ */
671
+ display_column_id?: string | null;
665
672
  /** True when this column is the table's display/identification column. */
666
673
  is_identification?: boolean;
667
674
  }
@@ -812,6 +819,21 @@ export const Linking: {
812
819
  canOpenURL(url: string): Promise<boolean>;
813
820
  };
814
821
 
822
+ /**
823
+ * sc-1179 — subscribe to the page-level refresh tick (pull-to-refresh on
824
+ * mobile, manual refresh button, etc.). The handler is called every time
825
+ * the host triggers a refresh; it may return a Promise — the host waits
826
+ * on all settled subscribers before clearing the refresh indicator.
827
+ *
828
+ * `useDatastoreQuery` / `useDatastoreRecord` / `useAsset` already
829
+ * auto-subscribe their own `refetch`, so widgets only call this directly
830
+ * to re-run non-datastore work. Safe to call on a host that does not
831
+ * implement refresh — collapses to a no-op there.
832
+ */
833
+ export function useRefresh(
834
+ handler: () => void | Promise<unknown>,
835
+ ): void;
836
+
815
837
  /**
816
838
  * Error class thrown by useDatastoreMutation callbacks (and surfaced by
817
839
  * useDatastoreQuery in its `error` slot). The `code` is a stable
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ export {
37
37
  useFill,
38
38
  useNavigation,
39
39
  useChildRenderer,
40
+ useRefresh,
40
41
  WidgetTree,
41
42
  } from "./hooks.js";
42
43
  // REQ-WSDK-PLATFORM §6 — Tier A hooks. Each ships in a per-platform file
@@ -35,6 +35,7 @@ export {
35
35
  useFill,
36
36
  useNavigation,
37
37
  useChildRenderer,
38
+ useRefresh,
38
39
  WidgetTree,
39
40
  } from "./hooks.js";
40
41
  // REQ-WSDK-PLATFORM §6 — Tier A hooks (native variants).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.40.2",
3
+ "version": "0.42.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",