@colixsystems/widget-sdk 0.98.0 → 0.100.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 +84 -3
- package/dist/contract.cjs +98 -9
- package/dist/contract.js +98 -9
- package/dist/hooks.js +4670 -4344
- package/dist/index.d.ts +95 -0
- package/dist/index.js +3 -0
- package/dist/index.native.js +3 -0
- package/dist/linter.cjs +68 -0
- package/dist/linter.js +68 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
6
6
|
|
|
7
7
|
| Injected at | Package | Surface (snake_case verbatim, `list` → `{ data, meta }`) |
|
|
8
8
|
| ----------- | ------- | -------- |
|
|
9
|
-
| `ctx.datastore` | `@colixsystems/datastore-client` | `tables.{list,get}`, `schema(tableId)`, `records(tableId).{ list(query), get(id), create(values), update(id,values) [PATCH], delete(id), aggregate(spec), permissions(recordId).{ list, grant, update, revoke } }` |
|
|
9
|
+
| `ctx.datastore` | `@colixsystems/datastore-client` | `tables.{list,get}`, `schema(tableId)`, `myPermissions(tableId, { recordId? })`, `records(tableId).{ list(query), get(id), create(values), update(id,values) [PATCH], delete(id), aggregate(spec), permissions(recordId).{ list, grant, update, revoke } }` |
|
|
10
10
|
| `ctx.directory` | `@colixsystems/directory-client` | `me()`, `users.{list,get,invite,deactivate,reactivate}`, `groups.{list,create,remove,addMember,removeMember,listMine}`, `invites.{list,revoke,resend}` |
|
|
11
11
|
| `ctx.assets` | `@colixsystems/assets-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useAsset()` (single asset by id) and `useAssetsByTag()` (every asset carrying a tag) resolve |
|
|
12
12
|
| `ctx.payments` | `@colixsystems/payments-client` | `requestPayment(body)`, `getPayment(id)` |
|
|
@@ -38,12 +38,15 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
38
38
|
| **CORE** | `useSpeechToText(options?)` | `{ transcript, partial, listening, supported, error, start, stop, abort, reset }` | `ctx.device.speech` — no scope. Dictation with the device's **on-device** recogniser: no audio is uploaded and no AI credit is spent. Capture is IMPERATIVE: call `start()` from a user gesture (a tap), never on mount. `transcript` accumulates finalised speech, `partial` holds the uncommitted guess (needs `options.interimResults`); `stop()` keeps it, `abort()` discards it. Rejects with `SpeechToTextError` (`.code` in `PERMISSION_DENIED \| NO_SPEECH \| LANGUAGE_UNSUPPORTED \| NETWORK \| ABORTED \| UNSUPPORTED \| INTERNAL`). **Gate your mic button on `supported`** — Firefox ships no `SpeechRecognition`. Identical on web (`SpeechRecognition`) and the Expo export (`expo-speech-recognition`). |
|
|
39
39
|
| **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. |
|
|
40
40
|
| **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. |
|
|
41
|
+
| **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". |
|
|
41
42
|
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
42
43
|
| **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
|
|
43
44
|
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
45
|
+
| **DATASTORE** | `useBoundColumns(tableId, shape, props)` | `{ columns, resolved, missing, loading, error }` | `schema(tableId)` (built on `useDatastoreSchema`) — `datastore.read:<table>`. Resolves author-bound column NAMES from `props` by exact name → case-insensitive name → first unclaimed column matching `shape[key].dataType`, so a column an author renamed after install still resolves instead of `record[props.titleField]` reading `undefined`. `columns` holds the resolved NAME (`record[columns.titleField]`); `resolved` holds the full `Column`; `missing` lists non-`optional` keys that never resolved. Falsy `tableId` collapses to `{ columns: {}, resolved: {}, missing: Object.keys(shape), loading: false, error: null }`. |
|
|
44
46
|
| **DATASTORE** | `useInterpretDraft(tableId)` | `{ interpret, interpreting, error, result, available }` | `interpret(tableId, body)` — `datastore.read:<table>`. Turns ONE sentence a user typed ("walk at 11 am tomorrow") into DRAFT column values so a form can prefill itself. IMPERATIVE: call `interpret(text, { fields?, timeZone? })` from an event handler, never on mount. It DRAFTS and writes nothing — show the values for review, then submit through `useDatastoreMutation().create`. Resolves to `{ values, unresolved }`; `values` is keyed by column NAME (the shape `create()` takes) and `unresolved` names the fields the sentence did not state. Only text / number / boolean / date / datetime / array columns are drafted — `FILE`, `RELATION`, `USER` and `USER_GROUP` carry ids and are never guessed. Fails closed to an empty draft. **Every call spends the workspace's AI credits** and is rate-limited per actor, so call it once per user action (never on mount or in a render loop); once the workspace runs out the call is refused with a generic 429 — an app user is deliberately **not** told the workspace's billing state, since they have never heard of an AI credit and cannot buy one. Never surface a raw error to the person filling the form: say drafting is unavailable and keep every field editable by hand. `available` is false where the host brokers no interpreter. |
|
|
45
47
|
| **DATASTORE** | `useDatastoreMutation(table)` | `{ create, update, delete }` | `records(table).{ create, update (PATCH), delete }` — `datastore.write:*` |
|
|
46
48
|
| **DATASTORE** | `useRecordPermissions(tableId, recordId)` | `{ permissions, loading, error, grant, revoke, update, refetch }` | `records(table).permissions(record).{ list, grant, update, revoke }` — `acl.write:records` (+ `can_grant` on the record) |
|
|
49
|
+
| **DATASTORE** | `useCanWrite(tableId, options?)` | `{ canWrite, loading, error, refetch }` | `myPermissions(tableId, { recordId? })` — scope `datastore.read:<table>`. A FLOOR, not a full replacement for domain-specific write rules: answers "is this caller signed in AND permitted", reading the same table-ACL answer the write endpoint enforces. Pass `{ recordId }` for a per-row check. A widget whose own rule is MORE SPECIFIC than the table ACL (e.g. "only the assigned user may edit this row") must still hand-check that in addition. Pair with `useUser()` to also tell "not signed in" apart from "signed in but forbidden" — both resolve `canWrite: false` here. Falsy `tableId`, or a host that hasn't injected `myPermissions` (an older host), collapses to `{ canWrite: false, loading: false, error: null, refetch: async () => undefined }` rather than throwing. |
|
|
47
50
|
| **FILES** (`ctx.assets`) | `useAsset(id)` | `{ url, file, loading, error, refetch }` | `ctx.assets.get` — no scope |
|
|
48
51
|
| **FILES** | `useAssetsByTag(tag, { type? })` | `{ assets, loading, error, refetch }` | `ctx.assets.list` (unwraps `{ data, meta }` to `assets`) — no scope. `type` defaults to `"image"`; pass `"all"` / `"audio"` / `"video"` / `"document"` to widen. Falsy `tag` collapses to `assets: []` without a round-trip. |
|
|
49
52
|
| **DIRECTORY** (`ctx.directory`) | `useDirectory(query?)` | `{ users, loading, error, refetch }` | `directory.users.list` — `directory.read:users` |
|
|
@@ -96,8 +99,32 @@ Host-integration surface only: no author-facing hook, prop, primitive, or manife
|
|
|
96
99
|
|
|
97
100
|
Host-integration surface only: no author-facing hook, prop, primitive, or manifest field changed. `CONTRACT.version` → `1.66.0`.
|
|
98
101
|
|
|
99
|
-
### What's new in 0.
|
|
102
|
+
### What's new in 0.100.0 (contract 1.74.0)
|
|
103
|
+
|
|
104
|
+
**Opt out of image compression on upload — `useFilestoreUpload({ compress })` (sc-5402).** A file uploaded through `POST /api/v1/filestore/files` now has its raster images compressed to WebP again (EXIF-stripped, longest edge capped at 4096 px), which is the right default for anything the app renders. When the ORIGINAL bytes matter — a document archive, a photo the user re-downloads, anything with an exact-bytes requirement — pass `compress: false`, either on the hook or per call:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const { upload } = useFilestoreUpload({ spaceType: 'personal', compress: false });
|
|
108
|
+
// …or per upload, which wins over the hook default:
|
|
109
|
+
await upload(file, { compress: false });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Only raster images are ever compressed. SVG keeps its vector, and video, audio, and documents are stored verbatim under both values — an upload is never queued for background transcoding. The opt-out is the only value put on the wire, so the backend stays the single source of the default.
|
|
113
|
+
|
|
114
|
+
`CONTRACT.version` → `1.74.0`. Additive — existing callers are byte-for-byte unchanged on the wire.
|
|
100
115
|
|
|
116
|
+
### What's new in 0.98.0 (contract 1.70.0)
|
|
117
|
+
|
|
118
|
+
**Three new hooks close the biggest gaps in the write-gating and query-authoring surface (sc-5206).**
|
|
119
|
+
|
|
120
|
+
- **`useBoundColumns(tableId, shape, props)`** — resolve author-bound column NAMES from a widget's own props, built on `useDatastoreSchema`. Falls back name → case-insensitive name → first unclaimed column matching `dataType`, so `record[props.titleField]` reading `undefined` after a tenant renames a column is no longer a widget's problem: read `record[bound.titleField]` (via `const { columns: bound } = useBoundColumns(tableId, shape, props)`) and it keeps resolving.
|
|
121
|
+
- **`useStableQuery(buildQuery)`** — the same content-diffed stable-reference trick `useDatastoreQuery` already applies to its own `query` argument, generalised into a reusable hook: `useDatastoreQuery(tableId, useStableQuery(() => ({...})))` replaces a hand-rolled `useMemo` and its easy-to-get-wrong deps array. Reads no `ctx` — safe outside a `WidgetContextProvider`.
|
|
122
|
+
- **`useCanWrite(tableId, options?)`** — a write-permission FLOOR reading a new `ctx.datastore.myPermissions` client method. `{ canWrite, loading, error, refetch }`; pass `{ recordId }` for a per-row check. Not a replacement for a MORE SPECIFIC domain rule (still hand-check "only the assigned user" in addition), and not a replacement for `useUser()` when the UI needs to tell "not signed in" apart from "signed in but forbidden".
|
|
123
|
+
|
|
124
|
+
The SDK linter gained two matching soft-warning rules: `raw-useMemo-into-datastore-query` (a `useDatastoreQuery` argument built from a raw `useMemo` when the file hasn't reached for `useStableQuery`) and `hand-rolled-write-gate` (a write gated on `useUser().groupIds`/`.roles` when the file hasn't reached for `useCanWrite`). Both are steering nudges (`severity: "warning"`), never publish-blocking.
|
|
125
|
+
|
|
126
|
+
- **`CONTRACT.version` → `1.70.0`** (additive: three new hooks + the new `datastore.myPermissions` context field + two linter rules). No existing export changed signature.
|
|
127
|
+
### What's new in 0.97.0 (contract 1.69.0)
|
|
101
128
|
**`CONTRACT.themeComponents` gains five scopes: `accent`, `destructive`, `muted`, `popover`, `ring` (sc-5392).** The vocabulary shipped with exactly `button`/`card`/`text` (sc-1497), so a shadcn/Tailwind app import's `--accent`, `--destructive`, `--muted`, `--popover` and `--ring` custom properties had no `themeConfig` home and were reported "no theme home" on every import. Each new scope binds to real `styleSchema` fields on the built-ins that already had a matching surface — `accent` (a highlight/tag surface: `background`/`borderColor`/`radius`) to the Label widget's own fields, `destructive` (a themed danger/delete action: `background`/`textColor`/`borderColor`) to a new "Danger" Button variant, `muted` (a subtle/secondary surface: `background`/`textColor`/`borderColor`/`radius`) to Form Input's and Form Builder's pre-existing input fields, `popover` (a dropdown/menu surface: `background`/`textColor`/`borderColor`) to the same two widgets' choice-field option list, and `ring` (the app-wide focus-visible outline: `color`/`width`) to a new emphasis border on Button. **This is HOST-ONLY plumbing, exactly like the three scopes before it** — `useTheme()`'s documented `components` slice is unchanged, no widget-authoring hook or `propertySchema` type moved, and no scope declares `universalFields`, so a third-party or AI-generated widget's contract is unaffected; the Developer guide and `DEFAULT_SYSTEM_PROMPT` need no update because neither ever documented this internal vocabulary. Fully additive: a theme with no `components` key, or one using only `button`/`card`/`text`, resolves exactly as before.
|
|
102
129
|
- **`CONTRACT.version` → `1.69.0`** (additive: five new `themeComponents` scopes + their target-field bindings). No existing scope, token, or export changed shape.
|
|
103
130
|
|
|
@@ -627,7 +654,7 @@ Also: `useFileSignatures(fileIds)` is now **self-scoped** (the caller's own sign
|
|
|
627
654
|
- `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. Every row carries a ready-to-render `url` (its absolutized `presigned_url`), so a gallery renders `files.map(f => f.url)` directly — no per-row `useFilestoreFile` call.
|
|
628
655
|
- `useFilestoreFile(fileId)` → `{ file, url, loading, error, refetch }` — resolves ONE file id to a displayable `url` (its `presigned_url`, absolutized for web + native) via `ctx.filestore.files.get(id)`. **Read the top-level `url`** — the returned `file` is `null` until the fetch resolves (and for an empty id), so `file.url` throws on the first render; it is correct only after a null check. **Never compose a file URL yourself** — the bytes are served only from a server-signed token URL, so a hand-built path like `/api/files/<id>` can never resolve (the linter's `no-host-api-url` rule flags it, including the `${location.origin}/api/files/…` form). A datastore `FILE` column holds — and reads back as — that **bare file-id string**; it is NOT hydrated into an object the way a `RELATION` (`{ id, label }`) or `USER` (`{ id, name }`) column is, so pass the value straight to the hook (no `{ id }` / `{ url }` unwrapping guard). Empty / deleted / not-found ids resolve to `url: null` so a display widget shows a fallback. When you render `url` in an `<Image>` that fills its container, size it with `aspectRatio` (e.g. `{ width: "100%", aspectRatio: 1 }`) or pixels — never a percentage `height`, which React Native collapses to 0 against a content-sized parent, so the image loads but is invisible. Requires `files.read:*`.
|
|
629
656
|
- `useFilestoreFolders({ spaceType, parentFolderId?, q?, enabled? })` → `{ folders, loading, error, refetch }` — the folder-navigation companion to `useFilestoreFiles`; pass `enabled:false` to suspend fetching.
|
|
630
|
-
- `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.
|
|
657
|
+
- `useFilestoreUpload({ spaceType, folderId?, compress? })` → `{ 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. Uploaded images are compressed to WebP by the backend; pass `compress: false` (on the hook, or per `upload(file, { compress })`) to store the file byte-for-byte — use it whenever the original matters. Pair with the `<FilePicker>` primitive for the visible trigger. Requires the `files.write:*` scope.
|
|
631
658
|
- `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).
|
|
632
659
|
|
|
633
660
|
`CONTRACT.version` → `1.20.0` (additive — no existing hook changed).
|
|
@@ -1032,6 +1059,60 @@ The manifest declares the matching scope:
|
|
|
1032
1059
|
|
|
1033
1060
|
The server-side gate is `canGrant` on the target record — Studio owners pass automatically; an APP_USER holds `canGrant` as the record's creator or via a delegated grant. A caller without `canGrant` receives `PermissionError { code: "FORBIDDEN" }`. The hook collapses to a stable no-op when `tableId` or `recordId` is null/empty — so a widget can render its picker first, then bind to the picked record without conditional hook tricks.
|
|
1034
1061
|
|
|
1062
|
+
## Resolving bound columns, a stable query, and a write-permission floor
|
|
1063
|
+
|
|
1064
|
+
`useBoundColumns`, `useStableQuery`, and `useCanWrite` (sc-5206) target the three most common ways a widget goes wrong against a table it doesn't fully control: a renamed column reading `undefined`, a query argument that refetches in a loop, and a write control offered to someone the table forbids.
|
|
1065
|
+
|
|
1066
|
+
```js
|
|
1067
|
+
import {
|
|
1068
|
+
Text,
|
|
1069
|
+
View,
|
|
1070
|
+
Pressable,
|
|
1071
|
+
useBoundColumns,
|
|
1072
|
+
useStableQuery,
|
|
1073
|
+
useDatastoreQuery,
|
|
1074
|
+
useCanWrite,
|
|
1075
|
+
useUser,
|
|
1076
|
+
} from "@colixsystems/widget-sdk";
|
|
1077
|
+
|
|
1078
|
+
export default function OpenTasks({ tableId, titleField, statusField }) {
|
|
1079
|
+
// Resolves to the CURRENT column names even if the author's binding is
|
|
1080
|
+
// stale (a column renamed after install), instead of reading `undefined`.
|
|
1081
|
+
const { columns: bound, loading: schemaLoading } = useBoundColumns(
|
|
1082
|
+
tableId,
|
|
1083
|
+
{ titleField: { dataType: "STRING" }, statusField: { dataType: "STRING" } },
|
|
1084
|
+
{ titleField, statusField },
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
// A binding is undefined until the schema resolves, so hold the query back
|
|
1088
|
+
// rather than filtering on an undefined column name.
|
|
1089
|
+
const ready = Boolean(bound.statusField);
|
|
1090
|
+
// A stable query argument with no useMemo deps array to get wrong.
|
|
1091
|
+
const query = useStableQuery(() => ({
|
|
1092
|
+
filter: ready ? { [bound.statusField]: "eq:open" } : {},
|
|
1093
|
+
sort: { field: "created_at", dir: "desc" },
|
|
1094
|
+
}));
|
|
1095
|
+
const { data, loading } = useDatastoreQuery(ready ? tableId : null, query);
|
|
1096
|
+
|
|
1097
|
+
const user = useUser();
|
|
1098
|
+
const { canWrite } = useCanWrite(tableId);
|
|
1099
|
+
|
|
1100
|
+
if (schemaLoading || loading) return <Text>Loading…</Text>;
|
|
1101
|
+
return (
|
|
1102
|
+
<View>
|
|
1103
|
+
{data.map((row) => <Text key={row.id}>{row[bound.titleField]}</Text>)}
|
|
1104
|
+
{!user.id ? (
|
|
1105
|
+
<Text>Sign in to add a task</Text>
|
|
1106
|
+
) : canWrite ? (
|
|
1107
|
+
<Pressable onPress={() => {/* … */}}><Text>Add task</Text></Pressable>
|
|
1108
|
+
) : null /* signed in but not permitted — no control, not a disabled one */}
|
|
1109
|
+
</View>
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
`useCanWrite` answers "is this table's ACL open to me" — it 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, typically with `options.recordId` passed to a second `useCanWrite` call or a plain `user.id === record[assigneeField]` comparison.
|
|
1115
|
+
|
|
1035
1116
|
## Cross-platform widgets (single-file vs split)
|
|
1036
1117
|
|
|
1037
1118
|
Every widget runs in **both** the web Player and the exported native (Expo) app.
|
package/dist/contract.cjs
CHANGED
|
@@ -510,6 +510,23 @@ const HOOKS = [
|
|
|
510
510
|
requiredContextSlice: ["i18n.locale"],
|
|
511
511
|
scopes: null,
|
|
512
512
|
},
|
|
513
|
+
{
|
|
514
|
+
name: "useStableQuery",
|
|
515
|
+
signature: "useStableQuery(buildQuery)",
|
|
516
|
+
description:
|
|
517
|
+
"sc-5206 — keep whatever buildQuery() returns at a STABLE reference across renders when its (JSON-serialised) content hasn't changed, so it can " +
|
|
518
|
+
"be passed straight into useDatastoreQuery's second argument (useDatastoreQuery(tableId, useStableQuery(() => ({...})))) instead of a hand-rolled " +
|
|
519
|
+
"useMemo with an easy-to-get-wrong deps array. This generalises the same trick useDatastoreQuery already applies internally to its own query " +
|
|
520
|
+
"argument. buildQuery is a ZERO-ARG function called every render; pure React state — no ctx, no host required, safe outside a " +
|
|
521
|
+
"WidgetContextProvider. Never throws: a buildQuery that itself throws degrades to a stable undefined, and a result JSON.stringify can't diff " +
|
|
522
|
+
"(e.g. a circular structure) degrades to \"always a new reference\".",
|
|
523
|
+
returnShape: {
|
|
524
|
+
"(returns)":
|
|
525
|
+
"T | undefined // the hook returns buildQuery()'s result directly, not a wrapper object",
|
|
526
|
+
},
|
|
527
|
+
requiredContextSlice: [],
|
|
528
|
+
scopes: null,
|
|
529
|
+
},
|
|
513
530
|
{
|
|
514
531
|
name: "useUser",
|
|
515
532
|
signature: "useUser()",
|
|
@@ -769,16 +786,21 @@ const HOOKS = [
|
|
|
769
786
|
},
|
|
770
787
|
{
|
|
771
788
|
name: "useFilestoreUpload",
|
|
772
|
-
signature: "useFilestoreUpload({ spaceType, folderId? })",
|
|
789
|
+
signature: "useFilestoreUpload({ spaceType, folderId?, compress? })",
|
|
773
790
|
description:
|
|
774
791
|
"Upload a file into the end-user's Filestore space. The widget passes " +
|
|
775
792
|
"the SPACE (`{ spaceType, folderId? }`); the hook resolves owner_id " +
|
|
776
793
|
"from the host context, builds a multipart FormData with the " +
|
|
777
794
|
"snake_case fields the backend reads verbatim (`space_type`, " +
|
|
778
795
|
"`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
|
|
779
|
-
"ctx.filestore.files.upload. `upload(file, { folderId? })`
|
|
780
|
-
"to the created file row or throws the wire error; a 404
|
|
781
|
-
"destination folder denied a write (REQ-FSH canWrite gate). " +
|
|
796
|
+
"ctx.filestore.files.upload. `upload(file, { folderId?, compress? })` " +
|
|
797
|
+
"resolves to the created file row or throws the wire error; a 404 " +
|
|
798
|
+
"means the destination folder denied a write (REQ-FSH canWrite gate). " +
|
|
799
|
+
"Uploaded images are compressed to WebP by the backend; pass " +
|
|
800
|
+
"`compress: false` (on the hook or per upload) to store the file " +
|
|
801
|
+
"byte-for-byte as the user provided it — use that whenever the " +
|
|
802
|
+
"ORIGINAL matters (a document archive, a photo the user re-downloads, " +
|
|
803
|
+
"anything with an exact-bytes requirement). " +
|
|
782
804
|
"ALWAYS pass spaceType explicitly — omitting it falls back to " +
|
|
783
805
|
"'project', which is unreadable by a logged-out visitor. Choose it by " +
|
|
784
806
|
"who must SEE the file: 'public' for content the app displays to " +
|
|
@@ -786,7 +808,7 @@ const HOOKS = [
|
|
|
786
808
|
"'project' for content restricted to signed-in workspace users, " +
|
|
787
809
|
"'personal' for a file private to the uploading app user.",
|
|
788
810
|
returnShape: {
|
|
789
|
-
upload: "(file, { folderId? }) => Promise<FilestoreFile>",
|
|
811
|
+
upload: "(file, { folderId?, compress? }) => Promise<FilestoreFile>",
|
|
790
812
|
uploading: "boolean",
|
|
791
813
|
error: "Error | null",
|
|
792
814
|
lastUploaded: "FilestoreFile | null",
|
|
@@ -960,6 +982,29 @@ const HOOKS = [
|
|
|
960
982
|
requiredContextSlice: ["datastore.schema"],
|
|
961
983
|
scopes: ["datastore.read:<table>"],
|
|
962
984
|
},
|
|
985
|
+
{
|
|
986
|
+
name: "useBoundColumns",
|
|
987
|
+
signature: "useBoundColumns(tableId, shape, props)",
|
|
988
|
+
description:
|
|
989
|
+
"sc-5206 — resolve author-bound column NAMES from a widget's own props, built on useDatastoreSchema. shape is " +
|
|
990
|
+
"{ [propKey]: { dataType?, optional? } }; props is the widget's own props carrying author-bound column names (e.g. props.titleField === " +
|
|
991
|
+
"'Title'). Resolution order per key: (1) an exact name match on props[key], (2) a case-insensitive name match, (3) the first column whose " +
|
|
992
|
+
"data_type matches shape[key].dataType that no EARLIER key in this call already claimed, (4) unresolved. Unresolved NEVER throws — the key " +
|
|
993
|
+
"reads undefined, so a column an author renamed after install still resolves instead of crashing the widget. Returns { columns, resolved, " +
|
|
994
|
+
"missing, loading, error }: columns holds the resolved column NAME — a drop-in replacement for record[props.titleField] -> " +
|
|
995
|
+
"record[columns.titleField]; resolved holds the full Column object (snake_case verbatim, as useDatastoreSchema returns it) so a caller can " +
|
|
996
|
+
"inspect resolved.<key>.data_type; missing lists every key whose spec is not optional:true and did not resolve. Falsy tableId collapses to " +
|
|
997
|
+
"{ columns: {}, resolved: {}, missing: Object.keys(shape), loading: false, error: null } without a network round-trip.",
|
|
998
|
+
returnShape: {
|
|
999
|
+
columns: "{ [propKey]: string | undefined }",
|
|
1000
|
+
resolved: "{ [propKey]: Column | undefined }",
|
|
1001
|
+
missing: "string[] // required (non-optional) keys that did not resolve",
|
|
1002
|
+
loading: "boolean",
|
|
1003
|
+
error: "DatastoreError | null",
|
|
1004
|
+
},
|
|
1005
|
+
requiredContextSlice: ["datastore.schema"],
|
|
1006
|
+
scopes: ["datastore.read:<table>"],
|
|
1007
|
+
},
|
|
963
1008
|
{
|
|
964
1009
|
name: "useInterpretDraft",
|
|
965
1010
|
signature: "useInterpretDraft(tableId)",
|
|
@@ -1331,6 +1376,28 @@ const HOOKS = [
|
|
|
1331
1376
|
requiredContextSlice: ["datastore.records"],
|
|
1332
1377
|
scopes: ["acl.write:records"],
|
|
1333
1378
|
},
|
|
1379
|
+
// sc-5206 — table/record write-permission floor, reading the injected
|
|
1380
|
+
// ctx.datastore.myPermissions.
|
|
1381
|
+
{
|
|
1382
|
+
name: "useCanWrite",
|
|
1383
|
+
signature: "useCanWrite(tableId, options?)",
|
|
1384
|
+
description:
|
|
1385
|
+
"sc-5206 — is the signed-in caller permitted to write to tableId (or, with options.recordId, to that one row)? Returns { canWrite, loading, " +
|
|
1386
|
+
"error, refetch }, reading the injected ctx.datastore.myPermissions(tableId, { recordId }). This is a FLOOR, not a full replacement for " +
|
|
1387
|
+
"domain-specific write rules: a widget whose rule is more specific than the table ACL (e.g. 'only the assigned user may edit this row') must " +
|
|
1388
|
+
"still hand-check that in addition to useCanWrite (user.id === record[assigneeField]). It only answers 'signed in AND permitted' — it does " +
|
|
1389
|
+
"NOT distinguish 'not signed in' from 'signed in but forbidden' (both resolve canWrite:false); pair it with useUser() when the UI needs to " +
|
|
1390
|
+
"tell those apart. myPermissions is a newer client method: a host that has not injected it is NOT a hard error — this hook collapses to " +
|
|
1391
|
+
"{ canWrite: false, loading: false, error: null, refetch: async () => undefined } rather than throwing. Same collapse when tableId is falsy.",
|
|
1392
|
+
returnShape: {
|
|
1393
|
+
canWrite: "boolean",
|
|
1394
|
+
loading: "boolean",
|
|
1395
|
+
error: "DatastoreError | null",
|
|
1396
|
+
refetch: "() => Promise<void>",
|
|
1397
|
+
},
|
|
1398
|
+
requiredContextSlice: ["datastore.myPermissions"],
|
|
1399
|
+
scopes: ["datastore.read:<table>"],
|
|
1400
|
+
},
|
|
1334
1401
|
// REQ-RT-07 — realtime table subscription.
|
|
1335
1402
|
{
|
|
1336
1403
|
name: "useDatastoreSubscription",
|
|
@@ -1862,13 +1929,19 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1862
1929
|
datastore: {
|
|
1863
1930
|
description:
|
|
1864
1931
|
"Injected @colixsystems/datastore-client instance. " +
|
|
1865
|
-
"{ tables: { list(), get(idOrName), interpret(idOrName, body) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
1932
|
+
"{ tables: { list(), get(idOrName), interpret(idOrName, body), myPermissions(idOrName, { recordId? }) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
1866
1933
|
"records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
|
|
1867
1934
|
"permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
|
|
1868
|
-
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing). " +
|
|
1935
|
+
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema() and useBoundColumns(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing); `myPermissions` backs useCanWrite() (sc-5206 — the caller's effective table/record write permission). " +
|
|
1869
1936
|
"List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
|
|
1870
1937
|
required: true,
|
|
1871
|
-
fields: {
|
|
1938
|
+
fields: {
|
|
1939
|
+
records: "function",
|
|
1940
|
+
schema: "function",
|
|
1941
|
+
tables: "object",
|
|
1942
|
+
interpret: "function",
|
|
1943
|
+
myPermissions: "function",
|
|
1944
|
+
},
|
|
1872
1945
|
},
|
|
1873
1946
|
directory: {
|
|
1874
1947
|
description:
|
|
@@ -3120,7 +3193,23 @@ const CONTRACT = deepFreeze({
|
|
|
3120
3193
|
// capped at five in TWO places -- a named constant on web and a bare `5`
|
|
3121
3194
|
// literal in the compiler -- so the number could drift between the hosts
|
|
3122
3195
|
// and no Studio surface could state it at all.
|
|
3123
|
-
|
|
3196
|
+
// 1.73.0: additive (sc-5206) — three new hooks: `useBoundColumns(tableId,
|
|
3197
|
+
// shape, props)` resolves author-bound column NAMES by name -> case-
|
|
3198
|
+
// insensitive name -> dataType fallback, so a renamed column still
|
|
3199
|
+
// resolves instead of the widget reading `record[props.titleField]`
|
|
3200
|
+
// directly and getting `undefined`; `useStableQuery(buildQuery)`
|
|
3201
|
+
// generalises `useDatastoreQuery`'s own content-diffed stable-reference
|
|
3202
|
+
// trick so a widget stops hand-rolling `useMemo` with a wrong deps
|
|
3203
|
+
// array; `useCanWrite(tableId, { recordId? })` reads a new
|
|
3204
|
+
// `ctx.datastore.myPermissions` client method as a write-permission
|
|
3205
|
+
// FLOOR, replacing the previous prompt-only "read useUser().groupIds /
|
|
3206
|
+
// .roles" guidance. No existing export changed signature.
|
|
3207
|
+
// 1.74.0: additive (sc-5402) — `useFilestoreUpload` accepts `compress`, on
|
|
3208
|
+
// the hook options and per `upload(file, { compress })`. Uploaded images
|
|
3209
|
+
// are compressed to WebP by default; `compress: false` stores the file
|
|
3210
|
+
// byte-for-byte. Existing callers are unaffected — the field is only sent
|
|
3211
|
+
// when the opt-out is chosen.
|
|
3212
|
+
version: "1.74.0",
|
|
3124
3213
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3125
3214
|
hooks: HOOKS,
|
|
3126
3215
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -510,6 +510,23 @@ const HOOKS = [
|
|
|
510
510
|
requiredContextSlice: ["i18n.locale"],
|
|
511
511
|
scopes: null,
|
|
512
512
|
},
|
|
513
|
+
{
|
|
514
|
+
name: "useStableQuery",
|
|
515
|
+
signature: "useStableQuery(buildQuery)",
|
|
516
|
+
description:
|
|
517
|
+
"sc-5206 — keep whatever buildQuery() returns at a STABLE reference across renders when its (JSON-serialised) content hasn't changed, so it can " +
|
|
518
|
+
"be passed straight into useDatastoreQuery's second argument (useDatastoreQuery(tableId, useStableQuery(() => ({...})))) instead of a hand-rolled " +
|
|
519
|
+
"useMemo with an easy-to-get-wrong deps array. This generalises the same trick useDatastoreQuery already applies internally to its own query " +
|
|
520
|
+
"argument. buildQuery is a ZERO-ARG function called every render; pure React state — no ctx, no host required, safe outside a " +
|
|
521
|
+
"WidgetContextProvider. Never throws: a buildQuery that itself throws degrades to a stable undefined, and a result JSON.stringify can't diff " +
|
|
522
|
+
"(e.g. a circular structure) degrades to \"always a new reference\".",
|
|
523
|
+
returnShape: {
|
|
524
|
+
"(returns)":
|
|
525
|
+
"T | undefined // the hook returns buildQuery()'s result directly, not a wrapper object",
|
|
526
|
+
},
|
|
527
|
+
requiredContextSlice: [],
|
|
528
|
+
scopes: null,
|
|
529
|
+
},
|
|
513
530
|
{
|
|
514
531
|
name: "useUser",
|
|
515
532
|
signature: "useUser()",
|
|
@@ -769,16 +786,21 @@ const HOOKS = [
|
|
|
769
786
|
},
|
|
770
787
|
{
|
|
771
788
|
name: "useFilestoreUpload",
|
|
772
|
-
signature: "useFilestoreUpload({ spaceType, folderId? })",
|
|
789
|
+
signature: "useFilestoreUpload({ spaceType, folderId?, compress? })",
|
|
773
790
|
description:
|
|
774
791
|
"Upload a file into the end-user's Filestore space. The widget passes " +
|
|
775
792
|
"the SPACE (`{ spaceType, folderId? }`); the hook resolves owner_id " +
|
|
776
793
|
"from the host context, builds a multipart FormData with the " +
|
|
777
794
|
"snake_case fields the backend reads verbatim (`space_type`, " +
|
|
778
795
|
"`owner_id`, `folder_id`, plus the binary `file`), and POSTs through " +
|
|
779
|
-
"ctx.filestore.files.upload. `upload(file, { folderId? })`
|
|
780
|
-
"to the created file row or throws the wire error; a 404
|
|
781
|
-
"destination folder denied a write (REQ-FSH canWrite gate). " +
|
|
796
|
+
"ctx.filestore.files.upload. `upload(file, { folderId?, compress? })` " +
|
|
797
|
+
"resolves to the created file row or throws the wire error; a 404 " +
|
|
798
|
+
"means the destination folder denied a write (REQ-FSH canWrite gate). " +
|
|
799
|
+
"Uploaded images are compressed to WebP by the backend; pass " +
|
|
800
|
+
"`compress: false` (on the hook or per upload) to store the file " +
|
|
801
|
+
"byte-for-byte as the user provided it — use that whenever the " +
|
|
802
|
+
"ORIGINAL matters (a document archive, a photo the user re-downloads, " +
|
|
803
|
+
"anything with an exact-bytes requirement). " +
|
|
782
804
|
"ALWAYS pass spaceType explicitly — omitting it falls back to " +
|
|
783
805
|
"'project', which is unreadable by a logged-out visitor. Choose it by " +
|
|
784
806
|
"who must SEE the file: 'public' for content the app displays to " +
|
|
@@ -786,7 +808,7 @@ const HOOKS = [
|
|
|
786
808
|
"'project' for content restricted to signed-in workspace users, " +
|
|
787
809
|
"'personal' for a file private to the uploading app user.",
|
|
788
810
|
returnShape: {
|
|
789
|
-
upload: "(file, { folderId? }) => Promise<FilestoreFile>",
|
|
811
|
+
upload: "(file, { folderId?, compress? }) => Promise<FilestoreFile>",
|
|
790
812
|
uploading: "boolean",
|
|
791
813
|
error: "Error | null",
|
|
792
814
|
lastUploaded: "FilestoreFile | null",
|
|
@@ -960,6 +982,29 @@ const HOOKS = [
|
|
|
960
982
|
requiredContextSlice: ["datastore.schema"],
|
|
961
983
|
scopes: ["datastore.read:<table>"],
|
|
962
984
|
},
|
|
985
|
+
{
|
|
986
|
+
name: "useBoundColumns",
|
|
987
|
+
signature: "useBoundColumns(tableId, shape, props)",
|
|
988
|
+
description:
|
|
989
|
+
"sc-5206 — resolve author-bound column NAMES from a widget's own props, built on useDatastoreSchema. shape is " +
|
|
990
|
+
"{ [propKey]: { dataType?, optional? } }; props is the widget's own props carrying author-bound column names (e.g. props.titleField === " +
|
|
991
|
+
"'Title'). Resolution order per key: (1) an exact name match on props[key], (2) a case-insensitive name match, (3) the first column whose " +
|
|
992
|
+
"data_type matches shape[key].dataType that no EARLIER key in this call already claimed, (4) unresolved. Unresolved NEVER throws — the key " +
|
|
993
|
+
"reads undefined, so a column an author renamed after install still resolves instead of crashing the widget. Returns { columns, resolved, " +
|
|
994
|
+
"missing, loading, error }: columns holds the resolved column NAME — a drop-in replacement for record[props.titleField] -> " +
|
|
995
|
+
"record[columns.titleField]; resolved holds the full Column object (snake_case verbatim, as useDatastoreSchema returns it) so a caller can " +
|
|
996
|
+
"inspect resolved.<key>.data_type; missing lists every key whose spec is not optional:true and did not resolve. Falsy tableId collapses to " +
|
|
997
|
+
"{ columns: {}, resolved: {}, missing: Object.keys(shape), loading: false, error: null } without a network round-trip.",
|
|
998
|
+
returnShape: {
|
|
999
|
+
columns: "{ [propKey]: string | undefined }",
|
|
1000
|
+
resolved: "{ [propKey]: Column | undefined }",
|
|
1001
|
+
missing: "string[] // required (non-optional) keys that did not resolve",
|
|
1002
|
+
loading: "boolean",
|
|
1003
|
+
error: "DatastoreError | null",
|
|
1004
|
+
},
|
|
1005
|
+
requiredContextSlice: ["datastore.schema"],
|
|
1006
|
+
scopes: ["datastore.read:<table>"],
|
|
1007
|
+
},
|
|
963
1008
|
{
|
|
964
1009
|
name: "useInterpretDraft",
|
|
965
1010
|
signature: "useInterpretDraft(tableId)",
|
|
@@ -1331,6 +1376,28 @@ const HOOKS = [
|
|
|
1331
1376
|
requiredContextSlice: ["datastore.records"],
|
|
1332
1377
|
scopes: ["acl.write:records"],
|
|
1333
1378
|
},
|
|
1379
|
+
// sc-5206 — table/record write-permission floor, reading the injected
|
|
1380
|
+
// ctx.datastore.myPermissions.
|
|
1381
|
+
{
|
|
1382
|
+
name: "useCanWrite",
|
|
1383
|
+
signature: "useCanWrite(tableId, options?)",
|
|
1384
|
+
description:
|
|
1385
|
+
"sc-5206 — is the signed-in caller permitted to write to tableId (or, with options.recordId, to that one row)? Returns { canWrite, loading, " +
|
|
1386
|
+
"error, refetch }, reading the injected ctx.datastore.myPermissions(tableId, { recordId }). This is a FLOOR, not a full replacement for " +
|
|
1387
|
+
"domain-specific write rules: a widget whose rule is more specific than the table ACL (e.g. 'only the assigned user may edit this row') must " +
|
|
1388
|
+
"still hand-check that in addition to useCanWrite (user.id === record[assigneeField]). It only answers 'signed in AND permitted' — it does " +
|
|
1389
|
+
"NOT distinguish 'not signed in' from 'signed in but forbidden' (both resolve canWrite:false); pair it with useUser() when the UI needs to " +
|
|
1390
|
+
"tell those apart. myPermissions is a newer client method: a host that has not injected it is NOT a hard error — this hook collapses to " +
|
|
1391
|
+
"{ canWrite: false, loading: false, error: null, refetch: async () => undefined } rather than throwing. Same collapse when tableId is falsy.",
|
|
1392
|
+
returnShape: {
|
|
1393
|
+
canWrite: "boolean",
|
|
1394
|
+
loading: "boolean",
|
|
1395
|
+
error: "DatastoreError | null",
|
|
1396
|
+
refetch: "() => Promise<void>",
|
|
1397
|
+
},
|
|
1398
|
+
requiredContextSlice: ["datastore.myPermissions"],
|
|
1399
|
+
scopes: ["datastore.read:<table>"],
|
|
1400
|
+
},
|
|
1334
1401
|
// REQ-RT-07 — realtime table subscription.
|
|
1335
1402
|
{
|
|
1336
1403
|
name: "useDatastoreSubscription",
|
|
@@ -1862,13 +1929,19 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1862
1929
|
datastore: {
|
|
1863
1930
|
description:
|
|
1864
1931
|
"Injected @colixsystems/datastore-client instance. " +
|
|
1865
|
-
"{ tables: { list(), get(idOrName), interpret(idOrName, body) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
1932
|
+
"{ tables: { list(), get(idOrName), interpret(idOrName, body), myPermissions(idOrName, { recordId? }) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
1866
1933
|
"records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
|
|
1867
1934
|
"permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
|
|
1868
|
-
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing). " +
|
|
1935
|
+
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema() and useBoundColumns(); `interpret` backs useInterpretDraft() (sc-4932 — drafts column values from one sentence; writes nothing); `myPermissions` backs useCanWrite() (sc-5206 — the caller's effective table/record write permission). " +
|
|
1869
1936
|
"List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
|
|
1870
1937
|
required: true,
|
|
1871
|
-
fields: {
|
|
1938
|
+
fields: {
|
|
1939
|
+
records: "function",
|
|
1940
|
+
schema: "function",
|
|
1941
|
+
tables: "object",
|
|
1942
|
+
interpret: "function",
|
|
1943
|
+
myPermissions: "function",
|
|
1944
|
+
},
|
|
1872
1945
|
},
|
|
1873
1946
|
directory: {
|
|
1874
1947
|
description:
|
|
@@ -3120,7 +3193,23 @@ const CONTRACT = deepFreeze({
|
|
|
3120
3193
|
// capped at five in TWO places -- a named constant on web and a bare `5`
|
|
3121
3194
|
// literal in the compiler -- so the number could drift between the hosts
|
|
3122
3195
|
// and no Studio surface could state it at all.
|
|
3123
|
-
|
|
3196
|
+
// 1.73.0: additive (sc-5206) — three new hooks: `useBoundColumns(tableId,
|
|
3197
|
+
// shape, props)` resolves author-bound column NAMES by name -> case-
|
|
3198
|
+
// insensitive name -> dataType fallback, so a renamed column still
|
|
3199
|
+
// resolves instead of the widget reading `record[props.titleField]`
|
|
3200
|
+
// directly and getting `undefined`; `useStableQuery(buildQuery)`
|
|
3201
|
+
// generalises `useDatastoreQuery`'s own content-diffed stable-reference
|
|
3202
|
+
// trick so a widget stops hand-rolling `useMemo` with a wrong deps
|
|
3203
|
+
// array; `useCanWrite(tableId, { recordId? })` reads a new
|
|
3204
|
+
// `ctx.datastore.myPermissions` client method as a write-permission
|
|
3205
|
+
// FLOOR, replacing the previous prompt-only "read useUser().groupIds /
|
|
3206
|
+
// .roles" guidance. No existing export changed signature.
|
|
3207
|
+
// 1.74.0: additive (sc-5402) — `useFilestoreUpload` accepts `compress`, on
|
|
3208
|
+
// the hook options and per `upload(file, { compress })`. Uploaded images
|
|
3209
|
+
// are compressed to WebP by default; `compress: false` stores the file
|
|
3210
|
+
// byte-for-byte. Existing callers are unaffected — the field is only sent
|
|
3211
|
+
// when the opt-out is chosen.
|
|
3212
|
+
version: "1.74.0",
|
|
3124
3213
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3125
3214
|
hooks: HOOKS,
|
|
3126
3215
|
primitives: PRIMITIVES,
|