@colixsystems/widget-sdk 0.57.0 → 0.59.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
@@ -52,7 +52,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
52
52
 
53
53
  ## Status
54
54
 
55
- `v0.57.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**.
55
+ `v0.59.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.59.0
58
+
59
+ **Display a stored file by id — `useFilestoreFile(fileId)` (sc-3031).** A new FILESTORE read hook that resolves ONE file id to a displayable URL. Given a file id the app stored — chiefly a datastore `FILE` column, which holds a filestore file id (a string), never bytes — it fetches the record via `ctx.filestore.files.get(id)` and returns `{ file, url, loading, error, refetch }`, where `url` is the record's `presigned_url` absolutized by the client so it renders on the web Player **and** the native Expo export alike. Drop `url` straight into `<Image source={{ uri: url }} />`. An empty id makes no round-trip (`{ file: null, url: null }`); a deleted / not-found id degrades the same way (`url` stays null, `error` carries the wire error) so a display widget shows its fallback instead of crashing. This closes the upload→store→display loop: `useFilestoreUpload(...).upload(file)` → write `file.id` into the `FILE` column → `useFilestoreFile(id).url` to show it. Requires the `files.read:*` scope. `CONTRACT.version` → `1.40.0`. Additive — one new read hook; no existing export changed signature.
60
+
61
+ ### What's new in 0.58.0
62
+
63
+ **Filter the directory by group (sc-2964).** `useDirectory(query?)` and `useUsers(query?)` gain an optional `group_id` on the query object (`{ q?, role?, is_active?, group_id?, limit?, offset? }`). Pass the `id` of a `useGroups()` row to list only that group's members — pair it with a group picker to build "members of group X". A `group_id` from another tenant or a non-existent one returns an empty roster, never a cross-tenant member. The `DirectoryQuery` / `UsersQuery` types add `group_id?: string`; existing callers pass no `group_id` and see the full roster. No scope, manifest field, import, or hook signature changed — `CONTRACT` is unchanged; the query flows verbatim through the injected `@colixsystems/directory-client` on both the web Player and the native Expo export.
56
64
 
57
65
  ### What's new in 0.57.0
58
66
 
@@ -206,6 +214,7 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
206
214
 
207
215
  **Filestore browsing + BankID file signing for widgets (REQ-FS / REQ-SIGN).** Three new hooks read a newly-injected `ctx.filestore` (the `@colixsystems/filestore-client`, now constructed by both the web and native hosts):
208
216
  - `useFilestoreFiles({ spaceType, folderId?, q?, type? })` → `{ files, loading, error, refetch }` — browses the end-user's Filestore space. The hook resolves `owner_id` from the host context (tenant for a project space, the app user for a personal space), so the widget only picks the space.
217
+ - `useFilestoreFile(fileId)` → `{ file, url, loading, error, refetch }` — resolves ONE file id (as held in a datastore `FILE` column) to a displayable `url` (its `presigned_url`, absolutized for web + native) via `ctx.filestore.files.get(id)`. Empty / deleted / not-found ids resolve to `url: null` so a display widget shows a fallback. Requires `files.read:*`.
209
218
  - `useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })` → `{ folders, loading, error, refetch }` — the folder-navigation companion to `useFilestoreFiles`; pass `enabled:false` to suspend fetching.
210
219
  - `useFilestoreUpload({ spaceType, folderId? })` → `{ upload, uploading, error, lastUploaded }` — POSTs a multipart upload to `ctx.filestore.files.upload`. Resolves `owner_id` from the host context (like the read hooks) so the widget only picks the space + destination folder. Pair with the `<FilePicker>` primitive for the visible trigger. Requires the `files.write:*` scope.
211
220
  - `useFileSignature(fileId)` → `{ status, qr, signerName, verdict, initiate, refresh, cancel, verify, … }` — drives a BankID signing flow for a file (the backend hashes the bytes server-side, binds the digest into the signature, and verifies the proof offline).
@@ -454,6 +463,7 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
454
463
 
455
464
  - **Pull spacing and corners from tokens.** Use `theme.spacing` (`xs / sm / md / lg / xl`) for a consistent padding and gap rhythm, and `theme.radii` (`sm / md / lg / pill`) for corners. Don't hardcode raw pixel values.
456
465
  - **Build a hierarchy.** A clear title (large, bold, `colors.onSurface`), body text, and muted captions in `colors.onSurfaceMuted` — three weights, not one flat size. Reserve `colors.primary` (with `colors.onPrimary` for text on it) for the single most important action or metric.
466
+ - **Set the theme font on every `Text`.** React Native `Text` does not inherit `fontFamily` from a parent, so a text element that omits it falls back to the system font and ignores the workspace's configured font. Put `theme.typography.fontFamily` on every text style (a shared `StyleSheet` built from `theme` keeps it in one place) and size text with `theme.typography.sizes`.
457
467
  - **Contain and elevate.** Wrap a logical unit in a surface: `colors.surface` + padding + `radii.md` + a `colors.border` hairline or a subtle shadow. Use the status roles (`danger / success / warning / info`) for state.
458
468
  - **Respond to touch.** Give every `Pressable` a pressed state via the function-style `style={({ pressed }) => [base, pressed && { opacity: 0.7 }]}`.
459
469
  - **Use icons for clarity.** Pair a `lucide-react-native` icon with its label at a consistent size, coloured from the theme.
package/dist/contract.cjs CHANGED
@@ -214,6 +214,29 @@ const HOOKS = [
214
214
  requiredContextSlice: ["filestore.files"],
215
215
  scopes: ["files.read:*"],
216
216
  },
217
+ {
218
+ name: "useFilestoreFile",
219
+ signature: "useFilestoreFile(fileId)",
220
+ description:
221
+ "Resolve ONE Filestore file id to a displayable URL. Given a file id " +
222
+ "the app stored (e.g. a datastore FILE column, which holds a filestore " +
223
+ "file id — never bytes), the hook fetches the record via " +
224
+ "ctx.filestore.files.get(id) and surfaces its `presigned_url` as `url`, " +
225
+ "ready to drop into <Image source={{ uri: url }} />. The URL is " +
226
+ "absolutized by the client so it loads on web AND native. An empty id " +
227
+ "collapses to { file: null, url: null } with no round-trip; a deleted / " +
228
+ "not-found id degrades the same way (url stays null, error carries the " +
229
+ "wire error) so a display widget shows its fallback instead of crashing.",
230
+ returnShape: {
231
+ file: "FilestoreFile | null",
232
+ url: "string | null",
233
+ loading: "boolean",
234
+ error: "Error | null",
235
+ refetch: "() => Promise<void>",
236
+ },
237
+ requiredContextSlice: ["filestore.files"],
238
+ scopes: ["files.read:*"],
239
+ },
217
240
  {
218
241
  name: "useFilestoreUpload",
219
242
  signature: "useFilestoreUpload({ spaceType, folderId? })",
@@ -1864,7 +1887,11 @@ const CONTRACT = deepFreeze({
1864
1887
  // resolved live by `created_at` desc with limit 1; recordId ignored). The
1865
1888
  // Data Value widget reads it to show a live "latest entry". Existing
1866
1889
  // bindings have no `mode` and read as static — additive, minor bump.
1867
- version: "1.39.0",
1890
+ // 1.40.0: additive (sc-3031) — `useFilestoreFile(fileId)` resolves one
1891
+ // filestore file id (as held in a datastore FILE column) to a
1892
+ // displayable `url` (its presigned_url, absolutized for web + native).
1893
+ // New read hook, no existing behaviour changes — minor bump.
1894
+ version: "1.40.0",
1868
1895
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1869
1896
  hooks: HOOKS,
1870
1897
  primitives: PRIMITIVES,
package/dist/contract.js CHANGED
@@ -214,6 +214,29 @@ const HOOKS = [
214
214
  requiredContextSlice: ["filestore.files"],
215
215
  scopes: ["files.read:*"],
216
216
  },
217
+ {
218
+ name: "useFilestoreFile",
219
+ signature: "useFilestoreFile(fileId)",
220
+ description:
221
+ "Resolve ONE Filestore file id to a displayable URL. Given a file id " +
222
+ "the app stored (e.g. a datastore FILE column, which holds a filestore " +
223
+ "file id — never bytes), the hook fetches the record via " +
224
+ "ctx.filestore.files.get(id) and surfaces its `presigned_url` as `url`, " +
225
+ "ready to drop into <Image source={{ uri: url }} />. The URL is " +
226
+ "absolutized by the client so it loads on web AND native. An empty id " +
227
+ "collapses to { file: null, url: null } with no round-trip; a deleted / " +
228
+ "not-found id degrades the same way (url stays null, error carries the " +
229
+ "wire error) so a display widget shows its fallback instead of crashing.",
230
+ returnShape: {
231
+ file: "FilestoreFile | null",
232
+ url: "string | null",
233
+ loading: "boolean",
234
+ error: "Error | null",
235
+ refetch: "() => Promise<void>",
236
+ },
237
+ requiredContextSlice: ["filestore.files"],
238
+ scopes: ["files.read:*"],
239
+ },
217
240
  {
218
241
  name: "useFilestoreUpload",
219
242
  signature: "useFilestoreUpload({ spaceType, folderId? })",
@@ -1864,7 +1887,11 @@ const CONTRACT = deepFreeze({
1864
1887
  // resolved live by `created_at` desc with limit 1; recordId ignored). The
1865
1888
  // Data Value widget reads it to show a live "latest entry". Existing
1866
1889
  // bindings have no `mode` and read as static — additive, minor bump.
1867
- version: "1.39.0",
1890
+ // 1.40.0: additive (sc-3031) — `useFilestoreFile(fileId)` resolves one
1891
+ // filestore file id (as held in a datastore FILE column) to a
1892
+ // displayable `url` (its presigned_url, absolutized for web + native).
1893
+ // New read hook, no existing behaviour changes — minor bump.
1894
+ version: "1.40.0",
1868
1895
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
1869
1896
  hooks: HOOKS,
1870
1897
  primitives: PRIMITIVES,
package/dist/hooks.js CHANGED
@@ -1502,6 +1502,80 @@ export function useFilestoreFiles(options) {
1502
1502
  return { files, loading, error, refetch };
1503
1503
  }
1504
1504
 
1505
+ /**
1506
+ * sc-3031 — resolve ONE Filestore file id to a displayable URL. Returns
1507
+ * `{ file, url, loading, error, refetch }`. Given a file id the app stored
1508
+ * (e.g. a datastore FILE column, which holds a filestore file id — never
1509
+ * bytes), the hook fetches the file record via `ctx.filestore.files.get(id)`
1510
+ * and surfaces its `presigned_url` as `url`, ready to drop straight into an
1511
+ * `<Image source={{ uri: url }} />`. The URL is absolutized by the filestore
1512
+ * client, so it loads on the web Player AND the native export.
1513
+ *
1514
+ * An empty / null id collapses to `{ file: null, url: null }` with no network
1515
+ * round-trip; a deleted or not-found id degrades the same way (`url` stays
1516
+ * null and `error` carries the wire error) so a display widget shows its
1517
+ * fallback instead of crashing.
1518
+ */
1519
+ export function useFilestoreFile(fileId) {
1520
+ const ctx = useWidgetContextOrThrow("useFilestoreFile");
1521
+ if (
1522
+ !ctx.filestore ||
1523
+ !ctx.filestore.files ||
1524
+ typeof ctx.filestore.files.get !== "function"
1525
+ ) {
1526
+ throw new Error(
1527
+ "useFilestoreFile: host did not inject a filestore client (ctx.filestore.files.get)",
1528
+ );
1529
+ }
1530
+
1531
+ const [file, setFile] = useState(null);
1532
+ const [loading, setLoading] = useState(Boolean(fileId));
1533
+ const [error, setError] = useState(null);
1534
+
1535
+ const filesRef = useRef(ctx.filestore.files);
1536
+ filesRef.current = ctx.filestore.files;
1537
+ const idRef = useRef(fileId);
1538
+ idRef.current = fileId;
1539
+ const runRef = useRef(0);
1540
+
1541
+ const doFetch = useCallback(async () => {
1542
+ const myRun = ++runRef.current;
1543
+ const id = idRef.current;
1544
+ if (!id) {
1545
+ setLoading(false);
1546
+ setError(null);
1547
+ setFile(null);
1548
+ return;
1549
+ }
1550
+ setLoading(true);
1551
+ setError(null);
1552
+ try {
1553
+ const row = await filesRef.current.get(id);
1554
+ if (runRef.current !== myRun) return;
1555
+ setFile(row || null);
1556
+ setLoading(false);
1557
+ } catch (err) {
1558
+ if (runRef.current !== myRun) return;
1559
+ setError(err);
1560
+ setFile(null);
1561
+ setLoading(false);
1562
+ }
1563
+ }, []);
1564
+
1565
+ useEffect(() => {
1566
+ doFetch();
1567
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1568
+ }, [fileId]);
1569
+
1570
+ const refetch = useCallback(async () => {
1571
+ await doFetch();
1572
+ }, [doFetch]);
1573
+
1574
+ const url =
1575
+ file && typeof file.presigned_url === "string" ? file.presigned_url : null;
1576
+ return { file, url, loading, error, refetch };
1577
+ }
1578
+
1505
1579
  /**
1506
1580
  * sc-1378 — upload a file into the end-user's Filestore space. Returns
1507
1581
  * `{ upload, uploading, error, lastUploaded }`. The widget passes the
@@ -2221,8 +2295,10 @@ function toDirectoryError(err) {
2221
2295
  * users is NOT part of this surface; the directory is read-only here (see
2222
2296
  * useUsers for administration).
2223
2297
  *
2224
- * `query` is an optional `{ q?, role?, is_active?, limit?, offset? }`
2225
- * object passed through verbatim. The hook re-fetches whenever
2298
+ * `query` is an optional `{ q?, role?, is_active?, group_id?, limit?, offset? }`
2299
+ * object passed through verbatim. `group_id` (the `id` of a `useGroups()` row)
2300
+ * restricts the roster to that group's members — pair it with a group picker to
2301
+ * build "members of group X". The hook re-fetches whenever
2226
2302
  * `JSON.stringify(query)` changes and exposes `refetch` for on-demand
2227
2303
  * reloads (e.g. a chat roster refresh).
2228
2304
  *
@@ -2300,8 +2376,10 @@ export function useDirectory(query) {
2300
2376
  * invite, deactivate, reactivate}` — `list` resolves to the `{ data, meta }`
2301
2377
  * envelope VERBATIM, so we unwrap `res.data` (default `[]`). User rows are
2302
2378
  * snake_case (`id`, `name`, `email`, `role`, `is_active`, …) and bodies
2303
- * (e.g. `{ email, name, group_ids? }`) pass through verbatim. The list
2304
- * refetches whenever `JSON.stringify(query)` changes; the imperative methods
2379
+ * (e.g. `{ email, name, group_ids? }`) pass through verbatim. `query` is an
2380
+ * optional `{ q?, role?, is_active?, group_id?, limit?, offset? }`; `group_id`
2381
+ * (the `id` of a `useGroups()` row) narrows the list to that group's members.
2382
+ * The list refetches whenever `JSON.stringify(query)` changes; the imperative methods
2305
2383
  * reject with a `DirectoryError`. Reads require the `users.read:*` scope;
2306
2384
  * mutations additionally require `users.write:*`. The host's signed
2307
2385
  * `X-Widget-Scopes` header + a tenant-scoped SystemAcl `users.read` /
package/dist/index.d.ts CHANGED
@@ -585,6 +585,11 @@ export interface DirectoryQuery {
585
585
  role?: "USER" | "INTEGRATION" | "ALL";
586
586
  /** Filter by active state (snake_case on the wire). */
587
587
  is_active?: boolean;
588
+ /**
589
+ * Restrict the roster to members of one app-user group — the `id` of a
590
+ * `useGroups()` row. Pair with a group picker to build "members of group X".
591
+ */
592
+ group_id?: string;
588
593
  limit?: number;
589
594
  offset?: number;
590
595
  }
@@ -1082,6 +1087,8 @@ export interface UsersQuery {
1082
1087
  q?: string;
1083
1088
  role?: "USER" | "INTEGRATION" | "ALL";
1084
1089
  is_active?: boolean;
1090
+ /** Restrict to members of one app-user group — the `id` of a `useGroups()` row. */
1091
+ group_id?: string;
1085
1092
  limit?: number;
1086
1093
  offset?: number;
1087
1094
  }
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  useAsset,
19
19
  useAssetsByTag,
20
20
  useFilestoreFiles,
21
+ useFilestoreFile,
21
22
  useFilestoreUpload,
22
23
  usePdfExport,
23
24
  useFilestoreFolders,
@@ -18,6 +18,7 @@ export {
18
18
  useAsset,
19
19
  useAssetsByTag,
20
20
  useFilestoreFiles,
21
+ useFilestoreFile,
21
22
  useFilestoreUpload,
22
23
  usePdfExport,
23
24
  useFilestoreFolders,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.57.0",
3
+ "version": "0.59.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",