@colixsystems/widget-sdk 0.172.0 → 0.174.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 +37 -4
- package/dist/contract.cjs +30 -7
- package/dist/contract.js +30 -7
- package/dist/hooks.js +21 -6
- package/dist/index.d.ts +29 -3
- package/dist/linter.cjs +40 -0
- package/dist/linter.js +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
25
25
|
| **CORE** | `useRouteParams()` | `{ [paramKey]: value }` | `ctx.navigation.currentRoute.params` — no scope. The nav params the previous page passed via `goTo(pageId, params)`; the flat accessor for master→detail (read `recordId` on a detail page). Empty object when none. |
|
|
26
26
|
| **CORE** | `usePageContext()` | `{ params, records }` | `ctx.pageContext` — no scope. The page's DECLARED parameters, resolved once by the host: `params` are coerced to their declared types, `records` holds the row already fetched for each `record` param (read it instead of fetching again). Both empty when the page declares none. |
|
|
27
27
|
| **CORE** | `useWidgetRoute(initial)` | `[state, setState]` | `ctx.widgetRoute` — no scope. Where YOUR WIDGET is, persisted by the host so it survives a reload and travels in a shared link (the `w_<instanceId>` query key on web, the screen's route params natively): the folder a browser has opened, a wizard step, a selected tab, a list's sort and search. `useState` semantics over an object — writes MERGE, `null` clears a key back to its `initial`, and `initial` is read once. Values are scalars or flat arrays of scalars, size- and length-capped; anything else is not stored. NOT history (Back still leaves the page, on both platforms). Degrades to component state on the Studio canvas. |
|
|
28
|
-
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope. The hook IS the emitter: `const emitSlot = useWidgetEvent("slotChosen")`, then `emitSlot(payload)`. Never destructure the result — there is no `emit` member. |
|
|
28
|
+
| **CORE** | `useWidgetEvent(name)` | `(payload?) => void` | `ctx.events.emit` — no scope. The hook IS the emitter: `const emitSlot = useWidgetEvent("slotChosen")`, then `emitSlot(payload)`. Never destructure the result — there is no `emit` member. A sibling's input reads the payload, and a page action the author binds to the event (Run action, Navigate) runs with it — fired from one control, only that control runs it. |
|
|
29
29
|
| **CORE** | `useWidgetInput(inputName)` | the published payload, or `undefined` | `ctx.inputs` — no scope. Reads a value ANOTHER widget on the same page published with `useWidgetEvent`. Declare the input in `manifest.inputs`; the page author wires it to one sibling's declared event. The channel retains the last payload, so a widget that mounts later still reads it. `undefined` while unwired or before the first publish — always render a sensible default. Page-scoped and ephemeral: use `useRouteParams()` for state that must survive navigation, the datastore for state that must persist. |
|
|
30
30
|
| **CORE** | `useChildRenderer()` | `{ renderNode(node) }` | `ctx.renderer` — no scope (prefer the `WidgetTree` component) |
|
|
31
31
|
| **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`. |
|
|
@@ -44,7 +44,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
44
44
|
| **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. |
|
|
45
45
|
| **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". |
|
|
46
46
|
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options)` (optional: options) | `{ data, loading, fetching, error, refetch }` — `loading` only until the first result for the table; a changed query / refetch keeps `data` while `fetching` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
47
|
-
| **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, fetching, error, refetch }` — a refetch of the same record keeps `data` while `fetching` | `records(table).get` — `datastore.read:<table>` |
|
|
47
|
+
| **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>` |
|
|
48
48
|
| **DATASTORE** | `useDatastoreSchema(tableId)` | `{ schema, loading, error, refetch }` | `schema(tableId)` — `datastore.read:<table>` |
|
|
49
49
|
| **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 }`. |
|
|
50
50
|
| **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. |
|
|
@@ -73,7 +73,38 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
73
73
|
|
|
74
74
|
## Status
|
|
75
75
|
|
|
76
|
-
`v0.
|
|
76
|
+
`v0.174.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**.
|
|
77
|
+
|
|
78
|
+
### What's new in 0.174.0 (contract 1.135.0)
|
|
79
|
+
|
|
80
|
+
**Show a row's Edit / Delete only to someone allowed to use it: `includeCapabilities`.** A list gated on `useCanWrite(tableId)` offered Edit and Delete on every row, because the table-level answer only says whether the user may *create*. On a table where each user owns the rows they created, that is true for every signed-in user, so the server refused every edit and delete on someone else's row. Ask the read for each row's own answer instead:
|
|
81
|
+
|
|
82
|
+
```jsx
|
|
83
|
+
const { data: rows, refetch } = useDatastoreQuery(tableId, { limit: 50, includeCapabilities: true });
|
|
84
|
+
const { delete: remove } = useDatastoreMutation(tableId);
|
|
85
|
+
// inside rows.map((row) => …)
|
|
86
|
+
{row._capabilities?.can_delete && (
|
|
87
|
+
<Pressable onPress={() => remove(row.id).then(refetch)}><Text>Delete</Text></Pressable>
|
|
88
|
+
)}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Every row arrives with `_capabilities: { can_read, can_write, can_delete, can_grant }`, the same answer `update` / `delete` enforce, computed for the whole page in one lookup. `useDatastoreRecord(tableId, recordId, { includeCapabilities: true })` does the same for one record. Without the option, rows are unchanged. The new linter warning `row-action-not-gated` flags a widget that calls `update` or `delete` with no per-row check (`_capabilities`, or `useCanWrite(tableId, { recordId })` on a single-record page).
|
|
92
|
+
|
|
93
|
+
### What's new in 0.173.1 (contract 1.134.1)
|
|
94
|
+
|
|
95
|
+
**An event your widget fires can run a page action, from the one control that fires it.** The author (or Mason) can bind Run action or Navigate to any event your manifest declares, and it runs when `useWidgetEvent` fires it. Fire the event from one control and only that control runs the action:
|
|
96
|
+
|
|
97
|
+
```jsx
|
|
98
|
+
// manifest: events: [{ name: "onBookPressed", payloadSchema: { classId: { type: "string" } } }]
|
|
99
|
+
const emitBook = useWidgetEvent("onBookPressed");
|
|
100
|
+
<Button title="Book" onPress={() => emitBook({ classId })} />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Never declare your own event as `onPress`: an `onPress` binding makes the whole widget one tap target, so a tap anywhere on it runs the action. Documentation only — both hosts already dispatched a named event's binding.
|
|
104
|
+
|
|
105
|
+
### What's new in 0.173.0 (contract 1.134.0)
|
|
106
|
+
|
|
107
|
+
**A `datastoreTemplate` RELATION may name any table in the template.** The installer now creates every table before any column, so `targetSuffix` may point at a table declared later in `tables`, and two tables may relate to each other both ways. A suffix no table in the template has is still rejected at publish. No change is needed to an existing template.
|
|
77
108
|
|
|
78
109
|
### What's new in 0.172.0 (contract 1.133.0)
|
|
79
110
|
|
|
@@ -2152,7 +2183,7 @@ export default function OpenTasks({ tableId, titleField, statusField }) {
|
|
|
2152
2183
|
}
|
|
2153
2184
|
```
|
|
2154
2185
|
|
|
2155
|
-
`useCanWrite` answers "
|
|
2186
|
+
`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]`).
|
|
2156
2187
|
|
|
2157
2188
|
## Cross-platform widgets (single-file vs split)
|
|
2158
2189
|
|
|
@@ -2223,6 +2254,8 @@ Pass `--manifest` to enable `style-field-unread` and the three editability rules
|
|
|
2223
2254
|
|
|
2224
2255
|
`flex-basis-percent` flags a literal percentage `flexBasis` (sc-7274) — `"100%"`, `"50%"`, or the ternary a responsive cell writes. `flex-basis` sizes the MAIN axis, so in a column parent it asks for the parent's HEIGHT: under an ancestor that makes that height definite (a Grid cell stretches its child on the web Player) the child fills the container and, since a `View` never shrinks, overflows over everything below it. Say full-width with `width: "100%"` — correct in a column AND in a wrap row — and keep a NUMBER for a wrap threshold. A warning, because the percentage is right in a row and a text scan cannot see the parent.
|
|
2225
2256
|
|
|
2257
|
+
`row-action-not-gated` flags a widget that calls `update` or `delete` from `useDatastoreMutation` without a per-row permission check (sc-9132): no `_capabilities` read and no `useCanWrite(tableId, { recordId })`. The table-level `useCanWrite(tableId)` only answers "may create", so a list gated on it shows Edit / Delete on rows the server will refuse. A warning.
|
|
2258
|
+
|
|
2226
2259
|
`no-html-in-content` flags an HTML tag inside a **string** (sc-6970) — `"<strong>"`, `"<br>"`, `"<p>…</p>"`. There is no HTML renderer on either host, so the tag reaches the reader as literal text; author formatted text with `<MarkdownInput>` and render it with `<RichText>` instead. Only string and template content is scanned, so your own `<View>` / `<Text>` JSX can never trip it. A warning for a human author, blocking for the AI widget agent.
|
|
2227
2260
|
|
|
2228
2261
|
## Local dev loop (`appstudio-widget dev`)
|
package/dist/contract.cjs
CHANGED
|
@@ -1018,11 +1018,14 @@ const HOOKS = [
|
|
|
1018
1018
|
},
|
|
1019
1019
|
{
|
|
1020
1020
|
name: "useDatastoreRecord",
|
|
1021
|
-
signature: "useDatastoreRecord(tableId, recordId)",
|
|
1021
|
+
signature: "useDatastoreRecord(tableId, recordId, options)",
|
|
1022
|
+
optionalArgs: ["options"],
|
|
1022
1023
|
description:
|
|
1023
1024
|
"Reads one row. Re-reading the same record (refetch, pull-to-refresh) " +
|
|
1024
1025
|
"keeps it in `data` with loading false while `fetching` is true; a " +
|
|
1025
|
-
"different table or record id resets to loading."
|
|
1026
|
+
"different table or record id resets to loading. " +
|
|
1027
|
+
"`{ includeCapabilities: true }` adds `_capabilities` to the row, as " +
|
|
1028
|
+
"for useDatastoreQuery.",
|
|
1026
1029
|
returnShape: {
|
|
1027
1030
|
data: "Record | null",
|
|
1028
1031
|
loading: "boolean",
|
|
@@ -1371,7 +1374,12 @@ const HOOKS = [
|
|
|
1371
1374
|
"until the first result for the bound table lands; a changed query on " +
|
|
1372
1375
|
"that table, refetch() and pull-to-refresh keep the rows already in " +
|
|
1373
1376
|
"`data` with loading false, while `fetching` is true. Binding a " +
|
|
1374
|
-
"different table resets to loading. Show a spinner on `loading` only."
|
|
1377
|
+
"different table resets to loading. Show a spinner on `loading` only. " +
|
|
1378
|
+
"Pass `includeCapabilities: true` in options to stamp each row with " +
|
|
1379
|
+
"`_capabilities` { can_read, can_write, can_delete, can_grant } — what " +
|
|
1380
|
+
"the signed-in user may do to THAT row, the answer update/delete " +
|
|
1381
|
+
"enforce, computed for the whole page in one lookup. Gate a row's " +
|
|
1382
|
+
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`.",
|
|
1375
1383
|
returnShape: {
|
|
1376
1384
|
data: "Record[]",
|
|
1377
1385
|
loading: "boolean",
|
|
@@ -1558,6 +1566,12 @@ const HOOKS = [
|
|
|
1558
1566
|
{
|
|
1559
1567
|
name: "useWidgetEvent",
|
|
1560
1568
|
signature: "useWidgetEvent(eventName)",
|
|
1569
|
+
description:
|
|
1570
|
+
"Fires one of the events the manifest declares. The page wires it two " +
|
|
1571
|
+
"ways: a sibling widget's input reads the payload, and an author-bound " +
|
|
1572
|
+
"page action (Run action, Navigate) runs with it. Fire a named event " +
|
|
1573
|
+
"from ONE control to have only that control run the action; an onPress " +
|
|
1574
|
+
"binding instead makes the whole widget the tap target.",
|
|
1561
1575
|
// sc-4753 — the hook IS the emitter, so its return is declared as a bare
|
|
1562
1576
|
// callable. Naming an `emit` member here made every derived surface tell
|
|
1563
1577
|
// authors to destructure a function, which yields undefined at run time.
|
|
@@ -1880,7 +1894,8 @@ const HOOKS = [
|
|
|
1880
1894
|
"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 " +
|
|
1881
1895
|
"still hand-check that in addition to useCanWrite (user.id === record[assigneeField]). It only answers 'signed in AND permitted' — it does " +
|
|
1882
1896
|
"NOT distinguish 'not signed in' from 'signed in but forbidden' (both resolve canWrite:false); pair it with useUser() when the UI needs to " +
|
|
1883
|
-
"tell those apart.
|
|
1897
|
+
"tell those apart. For a LIST, gate each row's edit/delete on useDatastoreQuery's `includeCapabilities` answer instead — a " +
|
|
1898
|
+
"{ recordId } check per row is one request per row, and the table-level answer only says whether they may CREATE. myPermissions is a newer client method: a host that has not injected it is NOT a hard error — this hook collapses to " +
|
|
1884
1899
|
"{ canWrite: false, loading: false, error: null, refetch: async () => undefined } rather than throwing. Same collapse when tableId is falsy.",
|
|
1885
1900
|
returnShape: {
|
|
1886
1901
|
canWrite: "boolean",
|
|
@@ -2633,7 +2648,10 @@ const MANIFEST_SCHEMA = {
|
|
|
2633
2648
|
"{ type, label? } — the SAME shape as propertySchema, NOT JSON Schema — " +
|
|
2634
2649
|
"where type is one of string, number, boolean, object, array. The payload reaches " +
|
|
2635
2650
|
"any sibling widget on the page whose inputs entry the page author wired " +
|
|
2636
|
-
"to this event."
|
|
2651
|
+
"to this event. An event can also run a page action the author binds to " +
|
|
2652
|
+
"it (Run action, Navigate): fired from one control with useWidgetEvent, " +
|
|
2653
|
+
"only that control runs the action. Never declare it as onPress, which " +
|
|
2654
|
+
"the host binds as a tap target over the whole widget.",
|
|
2637
2655
|
default: [],
|
|
2638
2656
|
},
|
|
2639
2657
|
inputs: {
|
|
@@ -2652,7 +2670,7 @@ const MANIFEST_SCHEMA = {
|
|
|
2652
2670
|
type: "object",
|
|
2653
2671
|
required: false,
|
|
2654
2672
|
description:
|
|
2655
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (
|
|
2673
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected.",
|
|
2656
2674
|
},
|
|
2657
2675
|
translations: {
|
|
2658
2676
|
type: "object",
|
|
@@ -4661,7 +4679,12 @@ const CONTRACT = deepFreeze({
|
|
|
4661
4679
|
// 1.133.0: additive (sc-9002) — `ListHeader` (column labels on `ListRow`'s frame);
|
|
4662
4680
|
// a `ListRow`'s lone `View` child spans the row; a selected row's bar
|
|
4663
4681
|
// no longer shifts its content. Other rows render as before.
|
|
4664
|
-
|
|
4682
|
+
// 1.134.1: docs (sc-9122) — an emitted event can run a bound action.
|
|
4683
|
+
// 1.135.0: additive (sc-9132) — `includeCapabilities` on useDatastoreQuery
|
|
4684
|
+
// options and a new `options` arg on useDatastoreRecord stamp each row
|
|
4685
|
+
// with `_capabilities`, so a widget shows Edit/Delete only on rows the
|
|
4686
|
+
// user may change. New linter warning `row-action-not-gated`.
|
|
4687
|
+
version: "1.135.0",
|
|
4665
4688
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4666
4689
|
hooks: HOOKS,
|
|
4667
4690
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1018,11 +1018,14 @@ const HOOKS = [
|
|
|
1018
1018
|
},
|
|
1019
1019
|
{
|
|
1020
1020
|
name: "useDatastoreRecord",
|
|
1021
|
-
signature: "useDatastoreRecord(tableId, recordId)",
|
|
1021
|
+
signature: "useDatastoreRecord(tableId, recordId, options)",
|
|
1022
|
+
optionalArgs: ["options"],
|
|
1022
1023
|
description:
|
|
1023
1024
|
"Reads one row. Re-reading the same record (refetch, pull-to-refresh) " +
|
|
1024
1025
|
"keeps it in `data` with loading false while `fetching` is true; a " +
|
|
1025
|
-
"different table or record id resets to loading."
|
|
1026
|
+
"different table or record id resets to loading. " +
|
|
1027
|
+
"`{ includeCapabilities: true }` adds `_capabilities` to the row, as " +
|
|
1028
|
+
"for useDatastoreQuery.",
|
|
1026
1029
|
returnShape: {
|
|
1027
1030
|
data: "Record | null",
|
|
1028
1031
|
loading: "boolean",
|
|
@@ -1371,7 +1374,12 @@ const HOOKS = [
|
|
|
1371
1374
|
"until the first result for the bound table lands; a changed query on " +
|
|
1372
1375
|
"that table, refetch() and pull-to-refresh keep the rows already in " +
|
|
1373
1376
|
"`data` with loading false, while `fetching` is true. Binding a " +
|
|
1374
|
-
"different table resets to loading. Show a spinner on `loading` only."
|
|
1377
|
+
"different table resets to loading. Show a spinner on `loading` only. " +
|
|
1378
|
+
"Pass `includeCapabilities: true` in options to stamp each row with " +
|
|
1379
|
+
"`_capabilities` { can_read, can_write, can_delete, can_grant } — what " +
|
|
1380
|
+
"the signed-in user may do to THAT row, the answer update/delete " +
|
|
1381
|
+
"enforce, computed for the whole page in one lookup. Gate a row's " +
|
|
1382
|
+
"Edit on `row._capabilities?.can_write` and Delete on `can_delete`.",
|
|
1375
1383
|
returnShape: {
|
|
1376
1384
|
data: "Record[]",
|
|
1377
1385
|
loading: "boolean",
|
|
@@ -1558,6 +1566,12 @@ const HOOKS = [
|
|
|
1558
1566
|
{
|
|
1559
1567
|
name: "useWidgetEvent",
|
|
1560
1568
|
signature: "useWidgetEvent(eventName)",
|
|
1569
|
+
description:
|
|
1570
|
+
"Fires one of the events the manifest declares. The page wires it two " +
|
|
1571
|
+
"ways: a sibling widget's input reads the payload, and an author-bound " +
|
|
1572
|
+
"page action (Run action, Navigate) runs with it. Fire a named event " +
|
|
1573
|
+
"from ONE control to have only that control run the action; an onPress " +
|
|
1574
|
+
"binding instead makes the whole widget the tap target.",
|
|
1561
1575
|
// sc-4753 — the hook IS the emitter, so its return is declared as a bare
|
|
1562
1576
|
// callable. Naming an `emit` member here made every derived surface tell
|
|
1563
1577
|
// authors to destructure a function, which yields undefined at run time.
|
|
@@ -1880,7 +1894,8 @@ const HOOKS = [
|
|
|
1880
1894
|
"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 " +
|
|
1881
1895
|
"still hand-check that in addition to useCanWrite (user.id === record[assigneeField]). It only answers 'signed in AND permitted' — it does " +
|
|
1882
1896
|
"NOT distinguish 'not signed in' from 'signed in but forbidden' (both resolve canWrite:false); pair it with useUser() when the UI needs to " +
|
|
1883
|
-
"tell those apart.
|
|
1897
|
+
"tell those apart. For a LIST, gate each row's edit/delete on useDatastoreQuery's `includeCapabilities` answer instead — a " +
|
|
1898
|
+
"{ recordId } check per row is one request per row, and the table-level answer only says whether they may CREATE. myPermissions is a newer client method: a host that has not injected it is NOT a hard error — this hook collapses to " +
|
|
1884
1899
|
"{ canWrite: false, loading: false, error: null, refetch: async () => undefined } rather than throwing. Same collapse when tableId is falsy.",
|
|
1885
1900
|
returnShape: {
|
|
1886
1901
|
canWrite: "boolean",
|
|
@@ -2629,7 +2644,10 @@ const MANIFEST_SCHEMA = {
|
|
|
2629
2644
|
"{ type, label? } — the SAME shape as propertySchema, NOT JSON Schema — " +
|
|
2630
2645
|
"where type is one of string, number, boolean, object, array. The payload reaches " +
|
|
2631
2646
|
"any sibling widget on the page whose inputs entry the page author wired " +
|
|
2632
|
-
"to this event."
|
|
2647
|
+
"to this event. An event can also run a page action the author binds to " +
|
|
2648
|
+
"it (Run action, Navigate): fired from one control with useWidgetEvent, " +
|
|
2649
|
+
"only that control runs the action. Never declare it as onPress, which " +
|
|
2650
|
+
"the host binds as a tap target over the whole widget.",
|
|
2633
2651
|
default: [],
|
|
2634
2652
|
},
|
|
2635
2653
|
inputs: {
|
|
@@ -2648,7 +2666,7 @@ const MANIFEST_SCHEMA = {
|
|
|
2648
2666
|
type: "object",
|
|
2649
2667
|
required: false,
|
|
2650
2668
|
description:
|
|
2651
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (
|
|
2669
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 8 tables, 24 columns per table. RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 8 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected.",
|
|
2652
2670
|
},
|
|
2653
2671
|
translations: {
|
|
2654
2672
|
type: "object",
|
|
@@ -4657,7 +4675,12 @@ const CONTRACT = deepFreeze({
|
|
|
4657
4675
|
// 1.133.0: additive (sc-9002) — `ListHeader` (column labels on `ListRow`'s frame);
|
|
4658
4676
|
// a `ListRow`'s lone `View` child spans the row; a selected row's bar
|
|
4659
4677
|
// no longer shifts its content. Other rows render as before.
|
|
4660
|
-
|
|
4678
|
+
// 1.134.1: docs (sc-9122) — an emitted event can run a bound action.
|
|
4679
|
+
// 1.135.0: additive (sc-9132) — `includeCapabilities` on useDatastoreQuery
|
|
4680
|
+
// options and a new `options` arg on useDatastoreRecord stamp each row
|
|
4681
|
+
// with `_capabilities`, so a widget shows Edit/Delete only on rows the
|
|
4682
|
+
// user may change. New linter warning `row-action-not-gated`.
|
|
4683
|
+
version: "1.135.0",
|
|
4661
4684
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4662
4685
|
hooks: HOOKS,
|
|
4663
4686
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -2185,7 +2185,8 @@ function serializeQuery(query) {
|
|
|
2185
2185
|
}
|
|
2186
2186
|
|
|
2187
2187
|
const queryResultKey = (table, queryKey) => JSON.stringify([table, queryKey]);
|
|
2188
|
-
const recordResultKey = (table, recordId
|
|
2188
|
+
const recordResultKey = (table, recordId, includeCapabilities = false) =>
|
|
2189
|
+
JSON.stringify([table, recordId, includeCapabilities]);
|
|
2189
2190
|
|
|
2190
2191
|
/**
|
|
2191
2192
|
* Stateful datastore query hook. Returns { data, loading, fetching, error,
|
|
@@ -2202,6 +2203,10 @@ const recordResultKey = (table, recordId) => JSON.stringify([table, recordId]);
|
|
|
2202
2203
|
* re-fetch when [table, JSON.stringify(query)] changes. `refetch` re-runs the
|
|
2203
2204
|
* same call on demand.
|
|
2204
2205
|
*
|
|
2206
|
+
* sc-9132 — `query.includeCapabilities: true` stamps each row with
|
|
2207
|
+
* `_capabilities` ({ can_read, can_write, can_delete, can_grant }), computed
|
|
2208
|
+
* for the whole page in one server lookup. Gate a row's Edit / Delete on it.
|
|
2209
|
+
*
|
|
2205
2210
|
* sc-8595 — stale-while-revalidate. `loading` is true only until the first
|
|
2206
2211
|
* result for the bound TABLE lands. A changed query on that table, `refetch()`
|
|
2207
2212
|
* and a page pull-to-refresh keep the rows already in `data` on screen with
|
|
@@ -2362,8 +2367,12 @@ export function useDatastoreQuery(table, query) {
|
|
|
2362
2367
|
* fetching: false, error: null, refetch } so the widget can render its empty state
|
|
2363
2368
|
* without throwing. A 404 surfaces as a DatastoreError with
|
|
2364
2369
|
* `code: "NOT_FOUND"`.
|
|
2370
|
+
*
|
|
2371
|
+
* sc-9132 — pass `{ includeCapabilities: true }` to have the row carry
|
|
2372
|
+
* `_capabilities` ({ can_read, can_write, can_delete, can_grant }): what the
|
|
2373
|
+
* signed-in user may do to THIS record, the same answer update/delete enforce.
|
|
2365
2374
|
*/
|
|
2366
|
-
export function useDatastoreRecord(table, recordId) {
|
|
2375
|
+
export function useDatastoreRecord(table, recordId, options) {
|
|
2367
2376
|
const ctx = useWidgetContextOrThrow("useDatastoreRecord");
|
|
2368
2377
|
if (!ctx.datastore || typeof ctx.datastore.records !== "function") {
|
|
2369
2378
|
throw new Error(
|
|
@@ -2378,11 +2387,14 @@ export function useDatastoreRecord(table, recordId) {
|
|
|
2378
2387
|
// Same ref discipline as useDatastoreQuery — `ctx` is a fresh object
|
|
2379
2388
|
// identity on every host render, so we hold the live inputs in refs
|
|
2380
2389
|
// to keep `refetch` a stable callback.
|
|
2390
|
+
const includeCapabilities = options?.includeCapabilities === true;
|
|
2381
2391
|
const tableRef = useRef(table);
|
|
2382
2392
|
const recordIdRef = useRef(recordId);
|
|
2393
|
+
const includeCapabilitiesRef = useRef(includeCapabilities);
|
|
2383
2394
|
const recordsRef = useRef(ctx.datastore.records);
|
|
2384
2395
|
tableRef.current = table;
|
|
2385
2396
|
recordIdRef.current = recordId;
|
|
2397
|
+
includeCapabilitiesRef.current = includeCapabilities;
|
|
2386
2398
|
recordsRef.current = ctx.datastore.records;
|
|
2387
2399
|
|
|
2388
2400
|
const runRef = useRef(0);
|
|
@@ -2396,11 +2408,13 @@ export function useDatastoreRecord(table, recordId) {
|
|
|
2396
2408
|
setSettled(NO_RECORD_RESULT);
|
|
2397
2409
|
return;
|
|
2398
2410
|
}
|
|
2399
|
-
const key = recordResultKey(t, id);
|
|
2411
|
+
const key = recordResultKey(t, id, includeCapabilitiesRef.current);
|
|
2400
2412
|
setInFlight(true);
|
|
2401
2413
|
try {
|
|
2402
2414
|
const ns = recordsRef.current(t);
|
|
2403
|
-
const row =
|
|
2415
|
+
const row = includeCapabilitiesRef.current
|
|
2416
|
+
? await ns.get(id, { includeCapabilities: true })
|
|
2417
|
+
: await ns.get(id);
|
|
2404
2418
|
if (runRef.current !== myRun) return;
|
|
2405
2419
|
setSettled({ key, data: row || null, error: null, loaded: true });
|
|
2406
2420
|
setInFlight(false);
|
|
@@ -2419,7 +2433,7 @@ export function useDatastoreRecord(table, recordId) {
|
|
|
2419
2433
|
useEffect(() => {
|
|
2420
2434
|
doFetch();
|
|
2421
2435
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2422
|
-
}, [table, recordId]);
|
|
2436
|
+
}, [table, recordId, includeCapabilities]);
|
|
2423
2437
|
|
|
2424
2438
|
const refetch = useCallback(async () => {
|
|
2425
2439
|
await doFetch();
|
|
@@ -2428,7 +2442,8 @@ export function useDatastoreRecord(table, recordId) {
|
|
|
2428
2442
|
// sc-1179 — auto-subscribe to the page-level refresh tick.
|
|
2429
2443
|
useRefresh(refetch);
|
|
2430
2444
|
|
|
2431
|
-
const currentKey =
|
|
2445
|
+
const currentKey =
|
|
2446
|
+
table && recordId ? recordResultKey(table, recordId, includeCapabilities) : null;
|
|
2432
2447
|
const busy = currentKey !== null && (settled.key !== currentKey || inFlight);
|
|
2433
2448
|
const hasRecord = currentKey !== null && settled.key === currentKey && settled.loaded;
|
|
2434
2449
|
return {
|
package/dist/index.d.ts
CHANGED
|
@@ -160,9 +160,10 @@ export interface WidgetInputDescriptor {
|
|
|
160
160
|
* - At most 24 columns per table. Same reason.
|
|
161
161
|
*
|
|
162
162
|
* RELATION columns reference siblings within the same template via
|
|
163
|
-
* `targetSuffix` — that suffix MUST belong to a table
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* `targetSuffix` — that suffix MUST belong to a table in the same array,
|
|
164
|
+
* declared anywhere in it; tables may relate to each other both ways.
|
|
165
|
+
* There is intentionally no way to reference a table outside the
|
|
166
|
+
* widget's own template.
|
|
166
167
|
*/
|
|
167
168
|
export interface WidgetDatastoreTemplateColumn {
|
|
168
169
|
name: string;
|
|
@@ -821,6 +822,23 @@ export interface Query {
|
|
|
821
822
|
sort?: Array<{ field: string; dir: "asc" | "desc" }>;
|
|
822
823
|
limit?: number;
|
|
823
824
|
cursor?: string;
|
|
825
|
+
/**
|
|
826
|
+
* Stamp each row with `_capabilities` — what the signed-in user may do to
|
|
827
|
+
* that row. Gate a row's Edit / Delete on it.
|
|
828
|
+
*/
|
|
829
|
+
includeCapabilities?: boolean;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* What the signed-in user may do to one record: the same answer the update /
|
|
834
|
+
* delete endpoints enforce. Present as `row._capabilities` only when the read
|
|
835
|
+
* passed `includeCapabilities: true`.
|
|
836
|
+
*/
|
|
837
|
+
export interface RecordCapabilities {
|
|
838
|
+
can_read: boolean;
|
|
839
|
+
can_write: boolean;
|
|
840
|
+
can_delete: boolean;
|
|
841
|
+
can_grant: boolean;
|
|
824
842
|
}
|
|
825
843
|
|
|
826
844
|
export interface QueryResult<T> {
|
|
@@ -896,6 +914,13 @@ export interface DirectoryResult {
|
|
|
896
914
|
*/
|
|
897
915
|
export function useDirectory(query?: DirectoryQuery): DirectoryResult;
|
|
898
916
|
|
|
917
|
+
/**
|
|
918
|
+
* The emitter for an event the manifest declares. A sibling's wired input
|
|
919
|
+
* reads the payload, and a page action the author binds to the event (Run
|
|
920
|
+
* action, Navigate) runs with it — fired from one control, only that control
|
|
921
|
+
* runs it. Never declare an event named `onPress`: that binding makes the
|
|
922
|
+
* whole widget one tap target.
|
|
923
|
+
*/
|
|
899
924
|
export function useWidgetEvent(name: string): (payload?: unknown) => void;
|
|
900
925
|
|
|
901
926
|
/**
|
|
@@ -1057,6 +1082,7 @@ export function useWidgetStyle(): Record<string, unknown>;
|
|
|
1057
1082
|
export function useDatastoreRecord(
|
|
1058
1083
|
tableId: string | null | undefined,
|
|
1059
1084
|
recordId: string | null | undefined,
|
|
1085
|
+
options?: { includeCapabilities?: boolean },
|
|
1060
1086
|
): {
|
|
1061
1087
|
data: unknown | null;
|
|
1062
1088
|
loading: boolean;
|
package/dist/linter.cjs
CHANGED
|
@@ -1189,6 +1189,45 @@ function _handRolledWriteGateRules(source) {
|
|
|
1189
1189
|
];
|
|
1190
1190
|
}
|
|
1191
1191
|
|
|
1192
|
+
// sc-9132 — soft warning: a row's Edit / Delete shown whatever that row's
|
|
1193
|
+
// permissions say. Table-level `useCanWrite(tableId)` only answers "may
|
|
1194
|
+
// create", which on a creator-owned table is true for every signed-in user,
|
|
1195
|
+
// so every row offered actions the server then refused.
|
|
1196
|
+
function _rowActionGatingRules(source) {
|
|
1197
|
+
const code = _stripNonCode(source);
|
|
1198
|
+
const call = _rowMutationCall(code);
|
|
1199
|
+
if (!call) return [];
|
|
1200
|
+
if (/\b_capabilities\b/.test(code)) return [];
|
|
1201
|
+
if (/\buseCanWrite\s*\([^)]*\brecordId\b/.test(code)) return [];
|
|
1202
|
+
const line = code.slice(0, call.index).split(/\r?\n/).length;
|
|
1203
|
+
return [
|
|
1204
|
+
{
|
|
1205
|
+
rule: "row-action-not-gated",
|
|
1206
|
+
severity: "warning",
|
|
1207
|
+
label:
|
|
1208
|
+
`edits or deletes rows without checking that row's permission - read ` +
|
|
1209
|
+
`with { includeCapabilities: true } and show Edit/Delete only when ` +
|
|
1210
|
+
`row._capabilities.can_write / can_delete is true.`,
|
|
1211
|
+
line,
|
|
1212
|
+
snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
|
|
1213
|
+
},
|
|
1214
|
+
];
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// The useDatastoreMutation() call whose update or delete the widget uses,
|
|
1218
|
+
// either as `m.update(` / `m.delete(` or destructured `{ update, delete }`.
|
|
1219
|
+
function _rowMutationCall(code) {
|
|
1220
|
+
const hookCall = /(?:\{([^}]*)\}|\b(\w+))\s*=\s*useDatastoreMutation\s*\(/g;
|
|
1221
|
+
for (const match of code.matchAll(hookCall)) {
|
|
1222
|
+
const [, destructured, name] = match;
|
|
1223
|
+
const usesRowVerb = destructured
|
|
1224
|
+
? /\b(update|delete)\b/.test(destructured)
|
|
1225
|
+
: new RegExp(`\\b${name}\\s*\\.\\s*(update|delete)\\s*\\(`).test(code);
|
|
1226
|
+
if (usesRowVerb) return match;
|
|
1227
|
+
}
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1192
1231
|
function _imagePercentHeightRules(source) {
|
|
1193
1232
|
const findings = [];
|
|
1194
1233
|
const code = _stripNonCode(source, { keepStrings: true });
|
|
@@ -1839,6 +1878,7 @@ function lintSource(source, options) {
|
|
|
1839
1878
|
// sc-5206 — soft warnings steering a widget toward the two new hooks.
|
|
1840
1879
|
findings.push(..._rawUseMemoIntoDatastoreQueryRules(source));
|
|
1841
1880
|
findings.push(..._handRolledWriteGateRules(source));
|
|
1881
|
+
findings.push(..._rowActionGatingRules(source));
|
|
1842
1882
|
findings.push(
|
|
1843
1883
|
..._scopeRules(source, options && options.manifest).map((f) => ({
|
|
1844
1884
|
...f,
|
package/dist/linter.js
CHANGED
|
@@ -1210,6 +1210,45 @@ function _handRolledWriteGateRules(source) {
|
|
|
1210
1210
|
];
|
|
1211
1211
|
}
|
|
1212
1212
|
|
|
1213
|
+
// sc-9132 — soft warning: a row's Edit / Delete shown whatever that row's
|
|
1214
|
+
// permissions say. Table-level `useCanWrite(tableId)` only answers "may
|
|
1215
|
+
// create", which on a creator-owned table is true for every signed-in user,
|
|
1216
|
+
// so every row offered actions the server then refused.
|
|
1217
|
+
function _rowActionGatingRules(source) {
|
|
1218
|
+
const code = _stripNonCode(source);
|
|
1219
|
+
const call = _rowMutationCall(code);
|
|
1220
|
+
if (!call) return [];
|
|
1221
|
+
if (/\b_capabilities\b/.test(code)) return [];
|
|
1222
|
+
if (/\buseCanWrite\s*\([^)]*\brecordId\b/.test(code)) return [];
|
|
1223
|
+
const line = code.slice(0, call.index).split(/\r?\n/).length;
|
|
1224
|
+
return [
|
|
1225
|
+
{
|
|
1226
|
+
rule: "row-action-not-gated",
|
|
1227
|
+
severity: "warning",
|
|
1228
|
+
label:
|
|
1229
|
+
`edits or deletes rows without checking that row's permission - read ` +
|
|
1230
|
+
`with { includeCapabilities: true } and show Edit/Delete only when ` +
|
|
1231
|
+
`row._capabilities.can_write / can_delete is true.`,
|
|
1232
|
+
line,
|
|
1233
|
+
snippet: (source.split(/\r?\n/)[line - 1] || "").trim().slice(0, 200),
|
|
1234
|
+
},
|
|
1235
|
+
];
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// The useDatastoreMutation() call whose update or delete the widget uses,
|
|
1239
|
+
// either as `m.update(` / `m.delete(` or destructured `{ update, delete }`.
|
|
1240
|
+
function _rowMutationCall(code) {
|
|
1241
|
+
const hookCall = /(?:\{([^}]*)\}|\b(\w+))\s*=\s*useDatastoreMutation\s*\(/g;
|
|
1242
|
+
for (const match of code.matchAll(hookCall)) {
|
|
1243
|
+
const [, destructured, name] = match;
|
|
1244
|
+
const usesRowVerb = destructured
|
|
1245
|
+
? /\b(update|delete)\b/.test(destructured)
|
|
1246
|
+
: new RegExp(`\\b${name}\\s*\\.\\s*(update|delete)\\s*\\(`).test(code);
|
|
1247
|
+
if (usesRowVerb) return match;
|
|
1248
|
+
}
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1213
1252
|
function _imagePercentHeightRules(source) {
|
|
1214
1253
|
const findings = [];
|
|
1215
1254
|
// Comments are blanked (string contents kept) so a commented-out example —
|
|
@@ -1915,6 +1954,7 @@ export function lintSource(source, options) {
|
|
|
1915
1954
|
// sc-5206 — soft warnings steering a widget toward the two new hooks.
|
|
1916
1955
|
findings.push(..._rawUseMemoIntoDatastoreQueryRules(source));
|
|
1917
1956
|
findings.push(..._handRolledWriteGateRules(source));
|
|
1957
|
+
findings.push(..._rowActionGatingRules(source));
|
|
1918
1958
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-aware rules. Run after the
|
|
1919
1959
|
// line-by-line scan so banned-identifier findings stay first in the
|
|
1920
1960
|
// output.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.174.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|