@colixsystems/widget-sdk 0.267.0 → 0.267.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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,11 @@ 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.267.1` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
90
+
91
+ ### What's new in 0.267.1 (contract 1.217.0)
92
+
93
+ **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
94
 
91
95
  ### What's new in 0.267.0 (contract 1.217.0)
92
96
 
@@ -3543,6 +3547,64 @@ export default function OpenTasks({ tableId, titleField, statusField }) {
3543
3547
 
3544
3548
  `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
3549
 
3550
+ ## Querying records
3551
+
3552
+ `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.
3553
+
3554
+ | Option | What the server does with it |
3555
+ | --- | --- |
3556
+ | `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 }]`. |
3557
+ | `filterMode` | `"and"` (the default) keeps rows matching every condition; `"or"` keeps rows matching any. |
3558
+ | `q` | Free-text search: a case-insensitive substring match across every text column of the table (encrypted columns excepted). |
3559
+ | `sort` | `{ field, dir }` with `dir` `"asc"` or `"desc"`, or the string `"field:desc"`. One column; `created_at` and `updated_at` work too. |
3560
+ | `limit` | Rows per page: 50 when left out, at most 1000. |
3561
+ | `offset` | Rows to skip, for the next page. |
3562
+ | `includeCapabilities` | `true` stamps each row with `_capabilities`, what the signed-in user may do to it. |
3563
+
3564
+ 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".
3565
+
3566
+ 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.
3567
+
3568
+ **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.
3569
+
3570
+ ```js
3571
+ import { useEffect, useState } from "react";
3572
+ import { Pressable, Text, TextInput, View, useDatastoreQuery, useStableQuery } from "@colixsystems/widget-sdk";
3573
+
3574
+ const PAGE = 50;
3575
+
3576
+ export default function ContactList({ tableId }) {
3577
+ const [draft, setDraft] = useState("");
3578
+ const [search, setSearch] = useState("");
3579
+ const [status, setStatus] = useState("");
3580
+ const [page, setPage] = useState(0);
3581
+ useEffect(() => {
3582
+ const wait = setTimeout(() => { setSearch(draft.trim()); setPage(0); }, 250);
3583
+ return () => clearTimeout(wait);
3584
+ }, [draft]);
3585
+ const query = useStableQuery(() => ({
3586
+ q: search,
3587
+ filter: status ? { status: `eq:${status}` } : undefined,
3588
+ sort: { field: "last_contacted", dir: "desc" },
3589
+ limit: PAGE,
3590
+ offset: page * PAGE,
3591
+ }));
3592
+ const { data, total, loading } = useDatastoreQuery(tableId, query);
3593
+ const pick = (next) => { setStatus(next); setPage(0); };
3594
+ return (
3595
+ <View>
3596
+ <TextInput value={draft} onChangeText={setDraft} placeholder="Search" />
3597
+ <Pressable onPress={() => pick(status === "Won" ? "" : "Won")}><Text>Won</Text></Pressable>
3598
+ {loading ? <Text>Loading…</Text> : data.map((row) => <Text key={row.id}>{row.name}</Text>)}
3599
+ <Text>{total ?? 0} contacts</Text>
3600
+ {(page + 1) * PAGE < (total ?? 0) ? (
3601
+ <Pressable onPress={() => setPage(page + 1)}><Text>Next page</Text></Pressable>
3602
+ ) : null}
3603
+ </View>
3604
+ );
3605
+ }
3606
+ ```
3607
+
3546
3608
  ## Cross-platform widgets (single-file vs split)
3547
3609
 
3548
3610
  Every widget runs in **both** the web Player and the exported native (Expo) app.
package/dist/index.d.ts CHANGED
@@ -1074,10 +1074,23 @@ export function hasCornerRadius(
1074
1074
  radius: ResolvedCornerRadius | null | undefined,
1075
1075
  ): boolean;
1076
1076
 
1077
+ /** A record query the datastore applies on the server; see the README's "Querying records". */
1077
1078
  export interface Query {
1078
- filter?: Record<string, unknown>;
1079
- sort?: Array<{ field: string; dir: "asc" | "desc" }>;
1079
+ /** `{ column: "op:value" }` (an array of expressions for a range), or `[{ column, operator, value, valueMode }]`. */
1080
+ filter?:
1081
+ | Record<string, string | string[]>
1082
+ | Array<{ column: string; operator?: string; value?: unknown; valueMode?: string }>;
1083
+ /** `"and"` (the default) keeps rows matching every condition, `"or"` rows matching any. */
1084
+ filterMode?: "and" | "or";
1085
+ /** A case-insensitive substring search across the table's text columns. */
1086
+ q?: string;
1087
+ /** `{ field, dir }` or `"field:desc"`; one column, so an array uses its first entry. */
1088
+ sort?: string | { field: string; dir?: "asc" | "desc" } | Array<{ field: string; dir?: "asc" | "desc" }>;
1089
+ /** Rows per page: 50 when omitted, at most 1000. */
1080
1090
  limit?: number;
1091
+ /** Rows to skip, for the next page. */
1092
+ offset?: number;
1093
+ /** @deprecated The datastore ignores it; page with `offset`. */
1081
1094
  cursor?: string;
1082
1095
  /**
1083
1096
  * Stamp each row with `_capabilities` — what the signed-in user may do to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.267.0",
3
+ "version": "0.267.1",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",