@colixsystems/widget-sdk 0.223.0 → 0.228.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 +63 -1
- package/dist/contract.cjs +99 -3
- package/dist/contract.js +99 -3
- package/dist/hooks.js +17 -2
- package/dist/index.d.ts +142 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/manifest.cjs +6 -0
- package/dist/manifest.js +6 -0
- package/dist/stats-hooks.js +60 -4
- package/package.json +1 -1
- package/src/contract.js +99 -3
package/README.md
CHANGED
|
@@ -84,7 +84,69 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
84
84
|
|
|
85
85
|
## Status
|
|
86
86
|
|
|
87
|
-
`v0.
|
|
87
|
+
`v0.228.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**.
|
|
88
|
+
|
|
89
|
+
### What's new in 0.228.0 (contract 1.184.0)
|
|
90
|
+
|
|
91
|
+
**A widget can ask for the whole screen — manifest `prefersFitToScreen` and `useFitScreen()`.** A page section can be set to "Fit to screen": it runs edge to edge across the app's content area (no page margins, no width cap) and takes the rest of the screen below the sections above it, on every screen size, in the Player and in the exported app. Set `prefersFitToScreen: true` in your manifest when your widget is built for that — it scrolls its own content inside the height it is given and keeps its controls in view, like a messenger, a map or a full-height board. In a fit section the host then gives your widget a box exactly the height left; a widget without the field grows with its content there, and the page scrolls. A maker who drops your widget at the top level of a page then gets the setting turned on for that section (they can switch it off, and it stays off), and the AppStudio app-builder fits the section it plans your widget into. Leave it `false` or omit it otherwise.
|
|
92
|
+
|
|
93
|
+
When your widget is that section's surface, `useFill()` is `true` and so is the new `useFitScreen()`. Drop your own frame then — border, rounded corners, shadow — so the widget meets the screen's edges cleanly; the host has already removed your outer margin.
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
import { useFill, useFitScreen, View } from "@colixsystems/widget-sdk";
|
|
97
|
+
|
|
98
|
+
export default function Board() {
|
|
99
|
+
const fill = useFill();
|
|
100
|
+
const edgeToEdge = useFitScreen();
|
|
101
|
+
return (
|
|
102
|
+
<View style={[fill ? { flex: 1, minHeight: 0 } : { height: 480 }, !edgeToEdge && { borderWidth: 1, borderRadius: 12 }]}>
|
|
103
|
+
{/* ... */}
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Additive: existing manifests omit the field and read `false`, and a host that never fits a widget leaves both hooks `false`. A published widget that must also run in an app whose Player is older than 0.228.0 reads the hook off a namespace import and treats a missing one as `false` (`import * as sdk from "@colixsystems/widget-sdk"; const useFitScreen = typeof sdk.useFitScreen === "function" ? sdk.useFitScreen : () => false;`, at module level): a named import of an export the host does not have stops the widget loading.
|
|
110
|
+
|
|
111
|
+
### What's new in 0.227.0 (contract 1.183.0)
|
|
112
|
+
|
|
113
|
+
**Columns the server keeps, and rules every client meets.** A `datastoreTemplate` can now say what a widget used to write itself and the server could not check:
|
|
114
|
+
|
|
115
|
+
- `latestVia: "<relation column on the target that points back>"` on a ONE_TO_ONE / ONE_TO_MANY relation (with `onDelete` left at `SET_NULL`; the relation back must have `inheritAcl: true` and `immutable: true`) keeps it pointing at the newest related row — the newest message of a conversation.
|
|
116
|
+
- `copyOf: { via, column }` keeps a column equal to `column` on the row that latest-row column points at — the last message's text, time and author on the conversation row. Text copied into a STRING column is cut to 140 characters; a copy of an encrypted column must be `encrypted` too, and a copy cannot `inheritAcl`.
|
|
117
|
+
|
|
118
|
+
Both are written only by the server, in the same write as the related row, so a list reads who wrote last, and when, without trusting whoever wrote the parent row. A client value for them is ignored; read them back.
|
|
119
|
+
|
|
120
|
+
A table may also declare `constraints`, checked on every write and refused as `CONSTRAINT_VIOLATION`: `unique` (now with `unlessRelated`, waived for rows whose related row has a yes/no column set), `no-overlap`, `related-open` (no row is added, changed or removed while the related row's column is filled — votes on a closed poll) and `related-index` (a whole number that must be a position in the related row's list — a poll option). Additive.
|
|
121
|
+
|
|
122
|
+
### What's new in 0.226.0 (contract 1.182.0)
|
|
123
|
+
|
|
124
|
+
**Rank by a column — `useDatastoreAggregate(tableId, { groupBy, order, limit })`.** Group a table by an ordinary column (text, number, yes/no, date or user) and get `groups: [{ group, count, sum }]`, counted on the server over every row the viewer can read, so it stays exact past the 1000-row page a list read returns. A user column's `group` is the user id, so a ranking is one call:
|
|
125
|
+
|
|
126
|
+
```jsx
|
|
127
|
+
const { groups } = useDatastoreAggregate(props.tableId, {
|
|
128
|
+
groupBy: "Reporter",
|
|
129
|
+
order: "count:desc",
|
|
130
|
+
limit: 10,
|
|
131
|
+
filter: { Reporter: "nempty:" },
|
|
132
|
+
});
|
|
133
|
+
// groups: [{ group: "<user id>", count: 42, sum: 0 }, …]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`order` is `count`, `sum` or `group`, then `:asc` or `:desc` (ascending when left out; by value when there is no `order`); `limit` is 1–1000. Rows with no value form one `Uncategorized` group. Encrypted, array, file and relation columns cannot be grouped.
|
|
137
|
+
|
|
138
|
+
### What's new in 0.225.0 (contract 1.181.0)
|
|
139
|
+
|
|
140
|
+
**Chart a table over time — `useDatastoreAggregate(tableId, spec)`.** Counts rows per UTC day or ISO week (Monday) on the server, over every row the viewer may read, so the series stays exact past the 1000-row page a list read returns. Every bucket of the range comes back, empty ones as `count: 0`, so a chart has no gaps. `groupBy` is `"created_at"` (new rows, the default) or `"updated_at"` (changed rows); any other column is refused with a `VALIDATION` error. `sumColumn` adds up a plain number column per bucket. At most 31 buckets.
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
const { buckets, loading } = useDatastoreAggregate(props.tableId, { bucket: "week", from: twelveWeeksAgo });
|
|
144
|
+
// buckets: [{ date: "2026-09-28", count: 14, sum: 0 }, …]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### What's new in 0.224.0 (contract 1.180.0)
|
|
148
|
+
|
|
149
|
+
**A logged-out visitor can attach files to the row they just created.** On a table that lets everyone create rows, the create → `upload(file, { recordId: row.id })` → `update(row.id, { File: stored.id })` flow now works without signing in, for a few minutes after the create. The host carries a grant for that one row from the create to its upload and update, so widget code does not change. The visitor cannot edit the row's other fields, read the file back on a private table, attach more than ten files, or attach to any other row; editing an existing row's files still needs a signed-in user. Additive.
|
|
88
150
|
|
|
89
151
|
### What's new in 0.223.0 (contract 1.179.0)
|
|
90
152
|
|
package/dist/contract.cjs
CHANGED
|
@@ -991,6 +991,24 @@ const HOOKS = [
|
|
|
991
991
|
requiredContextSlice: ["fill"],
|
|
992
992
|
scopes: null,
|
|
993
993
|
},
|
|
994
|
+
{
|
|
995
|
+
name: "useFitScreen",
|
|
996
|
+
signature: "useFitScreen()",
|
|
997
|
+
description:
|
|
998
|
+
"Returns true when this widget is the surface of a page section set to " +
|
|
999
|
+
"\"Fit to screen\": the section runs edge to edge across the app's " +
|
|
1000
|
+
"content area and takes the rest of the screen, so the widget should " +
|
|
1001
|
+
"drop its own frame (border, rounded corners, shadow) and meet the " +
|
|
1002
|
+
"edges cleanly. useFill() is true as well then, and the host has already " +
|
|
1003
|
+
"removed the widget's outer margin. Defaults to false wherever the host " +
|
|
1004
|
+
"has not fitted the widget, so calling it is always safe. The SAME value " +
|
|
1005
|
+
"is injected by the web Player and the native export.",
|
|
1006
|
+
returnShape: {
|
|
1007
|
+
"(returns)": "boolean",
|
|
1008
|
+
},
|
|
1009
|
+
requiredContextSlice: ["fitScreen"],
|
|
1010
|
+
scopes: null,
|
|
1011
|
+
},
|
|
994
1012
|
{
|
|
995
1013
|
name: "useChildRenderer",
|
|
996
1014
|
signature: "useChildRenderer()",
|
|
@@ -1397,7 +1415,8 @@ const HOOKS = [
|
|
|
1397
1415
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1398
1416
|
"recordId names the row the hook could not know at render — then write " +
|
|
1399
1417
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1400
|
-
"visitor
|
|
1418
|
+
"visitor may do this only for a row they just created, within a few " +
|
|
1419
|
+
"minutes, and never edits other rows' files. For a file that belongs to " +
|
|
1401
1420
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1402
1421
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1403
1422
|
"private to the uploading app user.",
|
|
@@ -1796,6 +1815,45 @@ const HOOKS = [
|
|
|
1796
1815
|
requiredContextSlice: ["datastore.records"],
|
|
1797
1816
|
scopes: ["datastore.read:*"],
|
|
1798
1817
|
},
|
|
1818
|
+
// sc-10942 — rows per day or week, counted on the server over every row the
|
|
1819
|
+
// viewer may read. Date columns only until column grouping is exact too.
|
|
1820
|
+
{
|
|
1821
|
+
name: "useDatastoreAggregate",
|
|
1822
|
+
signature:
|
|
1823
|
+
"useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
|
|
1824
|
+
optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
|
|
1825
|
+
description:
|
|
1826
|
+
"Counts rows on the server over EVERY row the viewer can read, so it is " +
|
|
1827
|
+
"exact past the 1000-row page a list read returns — never group " +
|
|
1828
|
+
"useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
|
|
1829
|
+
"number column. BY DATE (`groupBy` \"created_at\", the default, or " +
|
|
1830
|
+
"\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
|
|
1831
|
+
"(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
|
|
1832
|
+
"`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
|
|
1833
|
+
"now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
|
|
1834
|
+
"BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
|
|
1835
|
+
"column): one entry per value in `groups`. A USER column's `group` is the " +
|
|
1836
|
+
"user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
|
|
1837
|
+
"limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
|
|
1838
|
+
"or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
|
|
1839
|
+
"with no value form one \"Uncategorized\" group; on a user column, leave " +
|
|
1840
|
+
"them out with a `nempty:` filter. Encrypted, array, file and relation " +
|
|
1841
|
+
"columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
|
|
1842
|
+
"as useDatastoreQuery's, created_at ranges included. Pass null as the " +
|
|
1843
|
+
"table until a bound table is known.",
|
|
1844
|
+
returnShape: {
|
|
1845
|
+
buckets:
|
|
1846
|
+
"Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
|
|
1847
|
+
groups:
|
|
1848
|
+
"Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
|
|
1849
|
+
loading: "boolean",
|
|
1850
|
+
fetching: "boolean",
|
|
1851
|
+
error: "DatastoreError | null",
|
|
1852
|
+
refetch: "() => Promise<void>",
|
|
1853
|
+
},
|
|
1854
|
+
requiredContextSlice: ["datastore.records"],
|
|
1855
|
+
scopes: ["datastore.read:*"],
|
|
1856
|
+
},
|
|
1799
1857
|
{
|
|
1800
1858
|
name: "useDatastoreSchema",
|
|
1801
1859
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3532,6 +3590,13 @@ const MANIFEST_SCHEMA = {
|
|
|
3532
3590
|
"Optional (default false). TRUE when every platform entry also exports useBadgeCount(props): a hook returning the count the widget puts on its page's menu entry with usePageBadge, so the app keeps that badge live while the page is closed. The host runs counters for installed marketplace widgets (the workspace's own published widgets included); the platform's built-in widgets declare none. It calls the hook with the instance's resolved props, inside the widget's own context; the hook returns a whole number, or null while the count is not known yet. It only reads, and the host enforces that: the counter's datastore and directory expose only their read methods (plus realtime subscriptions) and notification mutes only get/list, while writes, typing signals, payments, notifications.send, files, assets, AI agents, translation, link previews, device capabilities, identification and account actions all answer as unavailable. It also renders nothing, navigates nowhere, emits no events or toasts, marks nothing read, and calls none of usePageBadge, useScreenTitle, useSectionEmpty or useWidgetRoute's setter. The linter refuses a declared counter whose entry has no useBadgeCount export. The AppStudio widget agent leaves this unset: a generated widget never declares a badge counter.",
|
|
3533
3591
|
default: false,
|
|
3534
3592
|
},
|
|
3593
|
+
prefersFitToScreen: {
|
|
3594
|
+
type: "boolean",
|
|
3595
|
+
required: false,
|
|
3596
|
+
description:
|
|
3597
|
+
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3598
|
+
default: false,
|
|
3599
|
+
},
|
|
3535
3600
|
events: {
|
|
3536
3601
|
type: "object[]",
|
|
3537
3602
|
required: true,
|
|
@@ -3563,7 +3628,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3563
3628
|
type: "object",
|
|
3564
3629
|
required: false,
|
|
3565
3630
|
description:
|
|
3566
|
-
"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. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table.",
|
|
3631
|
+
"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. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3567
3632
|
},
|
|
3568
3633
|
translations: {
|
|
3569
3634
|
type: "object",
|
|
@@ -3607,6 +3672,16 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3607
3672
|
required: false,
|
|
3608
3673
|
fields: {},
|
|
3609
3674
|
},
|
|
3675
|
+
// sc-11348 — optional host hint backing useFitScreen(): the widget is the
|
|
3676
|
+
// surface of a section set to Fit to screen, so it drops its own frame.
|
|
3677
|
+
fitScreen: {
|
|
3678
|
+
description:
|
|
3679
|
+
"Optional host layout hint (boolean). True when this widget is the " +
|
|
3680
|
+
"surface of a page section set to Fit to screen: edge to edge, the rest " +
|
|
3681
|
+
"of the screen. Backs useFitScreen(); defaults to false when absent.",
|
|
3682
|
+
required: false,
|
|
3683
|
+
fields: {},
|
|
3684
|
+
},
|
|
3610
3685
|
user: {
|
|
3611
3686
|
description:
|
|
3612
3687
|
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
@@ -5882,7 +5957,28 @@ const CONTRACT = deepFreeze({
|
|
|
5882
5957
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5883
5958
|
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5884
5959
|
// the signed-in user's own start page on both hosts.
|
|
5885
|
-
|
|
5960
|
+
// 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
|
|
5961
|
+
// the row they just created, for a few minutes: the host carries a grant
|
|
5962
|
+
// from the create to that row's upload and update. Widget code is the
|
|
5963
|
+
// same create → upload → update flow.
|
|
5964
|
+
// 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
|
|
5965
|
+
// counts rows per day or week on the server (date columns only), through
|
|
5966
|
+
// `records(t).aggregate`'s new bucket/from/to.
|
|
5967
|
+
// 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
|
|
5968
|
+
// ordinary column too, into `groups` ([{ group, count, sum }]), with
|
|
5969
|
+
// `order` and `limit` for a top N; counted on the server over every
|
|
5970
|
+
// readable row. A user column groups by user id.
|
|
5971
|
+
// 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
|
|
5972
|
+
// `latestVia` (a server-kept pointer to the newest related row) or
|
|
5973
|
+
// `copyOf: { via, column }` (a server-kept copy read through one), and
|
|
5974
|
+
// a table may declare `constraints` (`unique` with `unlessRelated`,
|
|
5975
|
+
// `no-overlap`, `related-open`, `related-index`).
|
|
5976
|
+
// 1.184.0: additive (sc-11348) — OPTIONAL manifest field
|
|
5977
|
+
// `prefersFitToScreen` (dropping the widget at a page's top level turns
|
|
5978
|
+
// that section's Fit to screen on), and `useFitScreen()` with its
|
|
5979
|
+
// optional `fitScreen` host hint: the widget is a fit section's surface,
|
|
5980
|
+
// so it drops its own frame. Existing manifests and hosts read false.
|
|
5981
|
+
version: "1.184.0",
|
|
5886
5982
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5887
5983
|
hooks: HOOKS,
|
|
5888
5984
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -991,6 +991,24 @@ const HOOKS = [
|
|
|
991
991
|
requiredContextSlice: ["fill"],
|
|
992
992
|
scopes: null,
|
|
993
993
|
},
|
|
994
|
+
{
|
|
995
|
+
name: "useFitScreen",
|
|
996
|
+
signature: "useFitScreen()",
|
|
997
|
+
description:
|
|
998
|
+
"Returns true when this widget is the surface of a page section set to " +
|
|
999
|
+
"\"Fit to screen\": the section runs edge to edge across the app's " +
|
|
1000
|
+
"content area and takes the rest of the screen, so the widget should " +
|
|
1001
|
+
"drop its own frame (border, rounded corners, shadow) and meet the " +
|
|
1002
|
+
"edges cleanly. useFill() is true as well then, and the host has already " +
|
|
1003
|
+
"removed the widget's outer margin. Defaults to false wherever the host " +
|
|
1004
|
+
"has not fitted the widget, so calling it is always safe. The SAME value " +
|
|
1005
|
+
"is injected by the web Player and the native export.",
|
|
1006
|
+
returnShape: {
|
|
1007
|
+
"(returns)": "boolean",
|
|
1008
|
+
},
|
|
1009
|
+
requiredContextSlice: ["fitScreen"],
|
|
1010
|
+
scopes: null,
|
|
1011
|
+
},
|
|
994
1012
|
{
|
|
995
1013
|
name: "useChildRenderer",
|
|
996
1014
|
signature: "useChildRenderer()",
|
|
@@ -1397,7 +1415,8 @@ const HOOKS = [
|
|
|
1397
1415
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1398
1416
|
"recordId names the row the hook could not know at render — then write " +
|
|
1399
1417
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1400
|
-
"visitor
|
|
1418
|
+
"visitor may do this only for a row they just created, within a few " +
|
|
1419
|
+
"minutes, and never edits other rows' files. For a file that belongs to " +
|
|
1401
1420
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1402
1421
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1403
1422
|
"private to the uploading app user.",
|
|
@@ -1796,6 +1815,45 @@ const HOOKS = [
|
|
|
1796
1815
|
requiredContextSlice: ["datastore.records"],
|
|
1797
1816
|
scopes: ["datastore.read:*"],
|
|
1798
1817
|
},
|
|
1818
|
+
// sc-10942 — rows per day or week, counted on the server over every row the
|
|
1819
|
+
// viewer may read. Date columns only until column grouping is exact too.
|
|
1820
|
+
{
|
|
1821
|
+
name: "useDatastoreAggregate",
|
|
1822
|
+
signature:
|
|
1823
|
+
"useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
|
|
1824
|
+
optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
|
|
1825
|
+
description:
|
|
1826
|
+
"Counts rows on the server over EVERY row the viewer can read, so it is " +
|
|
1827
|
+
"exact past the 1000-row page a list read returns — never group " +
|
|
1828
|
+
"useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
|
|
1829
|
+
"number column. BY DATE (`groupBy` \"created_at\", the default, or " +
|
|
1830
|
+
"\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
|
|
1831
|
+
"(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
|
|
1832
|
+
"`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
|
|
1833
|
+
"now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
|
|
1834
|
+
"BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
|
|
1835
|
+
"column): one entry per value in `groups`. A USER column's `group` is the " +
|
|
1836
|
+
"user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
|
|
1837
|
+
"limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
|
|
1838
|
+
"or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
|
|
1839
|
+
"with no value form one \"Uncategorized\" group; on a user column, leave " +
|
|
1840
|
+
"them out with a `nempty:` filter. Encrypted, array, file and relation " +
|
|
1841
|
+
"columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
|
|
1842
|
+
"as useDatastoreQuery's, created_at ranges included. Pass null as the " +
|
|
1843
|
+
"table until a bound table is known.",
|
|
1844
|
+
returnShape: {
|
|
1845
|
+
buckets:
|
|
1846
|
+
"Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
|
|
1847
|
+
groups:
|
|
1848
|
+
"Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
|
|
1849
|
+
loading: "boolean",
|
|
1850
|
+
fetching: "boolean",
|
|
1851
|
+
error: "DatastoreError | null",
|
|
1852
|
+
refetch: "() => Promise<void>",
|
|
1853
|
+
},
|
|
1854
|
+
requiredContextSlice: ["datastore.records"],
|
|
1855
|
+
scopes: ["datastore.read:*"],
|
|
1856
|
+
},
|
|
1799
1857
|
{
|
|
1800
1858
|
name: "useDatastoreSchema",
|
|
1801
1859
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3528,6 +3586,13 @@ const MANIFEST_SCHEMA = {
|
|
|
3528
3586
|
"Optional (default false). TRUE when every platform entry also exports useBadgeCount(props): a hook returning the count the widget puts on its page's menu entry with usePageBadge, so the app keeps that badge live while the page is closed. The host runs counters for installed marketplace widgets (the workspace's own published widgets included); the platform's built-in widgets declare none. It calls the hook with the instance's resolved props, inside the widget's own context; the hook returns a whole number, or null while the count is not known yet. It only reads, and the host enforces that: the counter's datastore and directory expose only their read methods (plus realtime subscriptions) and notification mutes only get/list, while writes, typing signals, payments, notifications.send, files, assets, AI agents, translation, link previews, device capabilities, identification and account actions all answer as unavailable. It also renders nothing, navigates nowhere, emits no events or toasts, marks nothing read, and calls none of usePageBadge, useScreenTitle, useSectionEmpty or useWidgetRoute's setter. The linter refuses a declared counter whose entry has no useBadgeCount export. The AppStudio widget agent leaves this unset: a generated widget never declares a badge counter.",
|
|
3529
3587
|
default: false,
|
|
3530
3588
|
},
|
|
3589
|
+
prefersFitToScreen: {
|
|
3590
|
+
type: "boolean",
|
|
3591
|
+
required: false,
|
|
3592
|
+
description:
|
|
3593
|
+
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3594
|
+
default: false,
|
|
3595
|
+
},
|
|
3531
3596
|
events: {
|
|
3532
3597
|
type: "object[]",
|
|
3533
3598
|
required: true,
|
|
@@ -3559,7 +3624,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3559
3624
|
type: "object",
|
|
3560
3625
|
required: false,
|
|
3561
3626
|
description:
|
|
3562
|
-
"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. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table.",
|
|
3627
|
+
"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. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3563
3628
|
},
|
|
3564
3629
|
translations: {
|
|
3565
3630
|
type: "object",
|
|
@@ -3603,6 +3668,16 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3603
3668
|
required: false,
|
|
3604
3669
|
fields: {},
|
|
3605
3670
|
},
|
|
3671
|
+
// sc-11348 — optional host hint backing useFitScreen(): the widget is the
|
|
3672
|
+
// surface of a section set to Fit to screen, so it drops its own frame.
|
|
3673
|
+
fitScreen: {
|
|
3674
|
+
description:
|
|
3675
|
+
"Optional host layout hint (boolean). True when this widget is the " +
|
|
3676
|
+
"surface of a page section set to Fit to screen: edge to edge, the rest " +
|
|
3677
|
+
"of the screen. Backs useFitScreen(); defaults to false when absent.",
|
|
3678
|
+
required: false,
|
|
3679
|
+
fields: {},
|
|
3680
|
+
},
|
|
3606
3681
|
user: {
|
|
3607
3682
|
description:
|
|
3608
3683
|
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
@@ -5878,7 +5953,28 @@ const CONTRACT = deepFreeze({
|
|
|
5878
5953
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5879
5954
|
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5880
5955
|
// the signed-in user's own start page on both hosts.
|
|
5881
|
-
|
|
5956
|
+
// 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
|
|
5957
|
+
// the row they just created, for a few minutes: the host carries a grant
|
|
5958
|
+
// from the create to that row's upload and update. Widget code is the
|
|
5959
|
+
// same create → upload → update flow.
|
|
5960
|
+
// 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
|
|
5961
|
+
// counts rows per day or week on the server (date columns only), through
|
|
5962
|
+
// `records(t).aggregate`'s new bucket/from/to.
|
|
5963
|
+
// 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
|
|
5964
|
+
// ordinary column too, into `groups` ([{ group, count, sum }]), with
|
|
5965
|
+
// `order` and `limit` for a top N; counted on the server over every
|
|
5966
|
+
// readable row. A user column groups by user id.
|
|
5967
|
+
// 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
|
|
5968
|
+
// `latestVia` (a server-kept pointer to the newest related row) or
|
|
5969
|
+
// `copyOf: { via, column }` (a server-kept copy read through one), and
|
|
5970
|
+
// a table may declare `constraints` (`unique` with `unlessRelated`,
|
|
5971
|
+
// `no-overlap`, `related-open`, `related-index`).
|
|
5972
|
+
// 1.184.0: additive (sc-11348) — OPTIONAL manifest field
|
|
5973
|
+
// `prefersFitToScreen` (dropping the widget at a page's top level turns
|
|
5974
|
+
// that section's Fit to screen on), and `useFitScreen()` with its
|
|
5975
|
+
// optional `fitScreen` host hint: the widget is a fit section's surface,
|
|
5976
|
+
// so it drops its own frame. Existing manifests and hosts read false.
|
|
5977
|
+
version: "1.184.0",
|
|
5882
5978
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5883
5979
|
hooks: HOOKS,
|
|
5884
5980
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -488,6 +488,19 @@ export function useFill() {
|
|
|
488
488
|
return ctx.fill === true;
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Returns `true` when this widget is the surface of a page section set to
|
|
493
|
+
* "Fit to screen": the section runs edge to edge across the app's content
|
|
494
|
+
* area and takes the rest of the screen. Drop the widget's own frame then —
|
|
495
|
+
* border, rounded corners, shadow — so it meets the edges cleanly; `useFill()`
|
|
496
|
+
* is `true` as well, and the host has already removed the outer margin.
|
|
497
|
+
* Defaults to `false`, and both hosts inject the same value.
|
|
498
|
+
*/
|
|
499
|
+
export function useFitScreen() {
|
|
500
|
+
const ctx = useWidgetContextOrThrow("useFitScreen");
|
|
501
|
+
return ctx.fitScreen === true;
|
|
502
|
+
}
|
|
503
|
+
|
|
491
504
|
/**
|
|
492
505
|
* Returns the host's child-node renderer:
|
|
493
506
|
* { renderNode(node) }.
|
|
@@ -2706,7 +2719,8 @@ function isRetryableDomainStatus(status) {
|
|
|
2706
2719
|
return status >= 500;
|
|
2707
2720
|
}
|
|
2708
2721
|
|
|
2709
|
-
|
|
2722
|
+
// Internal: shared with stats-hooks.js, not re-exported by the index.
|
|
2723
|
+
export function toDatastoreError(err) {
|
|
2710
2724
|
// An already-mapped SDK error passes straight through. NOTE: the datastore
|
|
2711
2725
|
// CLIENT exports its own class of the same name, so a client rejection does
|
|
2712
2726
|
// NOT match here — it is handled by the envelope read below, which is exactly
|
|
@@ -4804,7 +4818,8 @@ export function useFilestoreUpload(options) {
|
|
|
4804
4818
|
setUploading(true);
|
|
4805
4819
|
setError(null);
|
|
4806
4820
|
try {
|
|
4807
|
-
|
|
4821
|
+
// sc-11244: names the row, so a visitor's grant for it can travel.
|
|
4822
|
+
const created = await filesRef.current.upload(form, isRecordSpace ? { recordId: ownerId } : undefined);
|
|
4808
4823
|
setLastUploaded(created || null);
|
|
4809
4824
|
setUploading(false);
|
|
4810
4825
|
return created;
|
package/dist/index.d.ts
CHANGED
|
@@ -228,8 +228,59 @@ export interface WidgetDatastoreTemplateColumn {
|
|
|
228
228
|
* `MANY_TO_MANY` relation, whose links live outside the row.
|
|
229
229
|
*/
|
|
230
230
|
immutable?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* ONE_TO_ONE / ONE_TO_MANY RELATION only, with `onDelete` left at
|
|
233
|
+
* `SET_NULL`: the server keeps this column pointing at the newest row of its
|
|
234
|
+
* target table whose relation column named here points back at this row.
|
|
235
|
+
* That relation must have `inheritAcl: true`, so only rows this row's
|
|
236
|
+
* readers can read are followed, and `immutable: true`, so a row never
|
|
237
|
+
* moves under another parent. Written only by the server; any value a
|
|
238
|
+
* client sends is ignored.
|
|
239
|
+
*/
|
|
240
|
+
latestVia?: string;
|
|
241
|
+
/**
|
|
242
|
+
* The server keeps this column equal to `column` on the row the latest-row
|
|
243
|
+
* column `via` (on this table) points at: the same type, or text into
|
|
244
|
+
* text, cut to 140 characters in a STRING column. A copy of an encrypted
|
|
245
|
+
* column must be `encrypted` too, and a copy cannot `inheritAcl`. Written
|
|
246
|
+
* only by the server, so a list can show who wrote the newest row, and
|
|
247
|
+
* when, without trusting whoever wrote this one.
|
|
248
|
+
*/
|
|
249
|
+
copyOf?: { via: string; column: string };
|
|
231
250
|
}
|
|
232
251
|
|
|
252
|
+
/**
|
|
253
|
+
* A rule the server checks on every write to the table, for every client. A
|
|
254
|
+
* refused write is a `CONSTRAINT_VIOLATION`.
|
|
255
|
+
*/
|
|
256
|
+
export type WidgetDatastoreTemplateConstraint =
|
|
257
|
+
| {
|
|
258
|
+
/** No two rows share these values; `unlessRelated` waives it for rows
|
|
259
|
+
* whose related row has that yes/no column set. */
|
|
260
|
+
type: "unique";
|
|
261
|
+
config: {
|
|
262
|
+
columns: string[];
|
|
263
|
+
partitionColumns?: string[];
|
|
264
|
+
unlessRelated?: { relation: string; column: string };
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
| {
|
|
268
|
+
/** No two rows' `startColumn`..`endColumn` intervals overlap. */
|
|
269
|
+
type: "no-overlap";
|
|
270
|
+
config: { startColumn: string; endColumn: string; partitionColumns?: string[] };
|
|
271
|
+
}
|
|
272
|
+
| {
|
|
273
|
+
/** No row is added, changed or removed while its related row's
|
|
274
|
+
* `closedColumn` is filled (a yes/no column: while it is yes). */
|
|
275
|
+
type: "related-open";
|
|
276
|
+
config: { relation: string; closedColumn: string };
|
|
277
|
+
}
|
|
278
|
+
| {
|
|
279
|
+
/** `column` must be a position (0-based) in the related row's list. */
|
|
280
|
+
type: "related-index";
|
|
281
|
+
config: { column: string; relation: string; listColumn: string };
|
|
282
|
+
};
|
|
283
|
+
|
|
233
284
|
export interface WidgetDatastoreTemplateTable {
|
|
234
285
|
/** Stable identifier within the template; appears in the created table name. */
|
|
235
286
|
suffix: string;
|
|
@@ -267,6 +318,8 @@ export interface WidgetDatastoreTemplateTable {
|
|
|
267
318
|
rows?: Array<
|
|
268
319
|
Record<string, string | number | boolean | string[] | number[] | null>
|
|
269
320
|
>;
|
|
321
|
+
/** Rules the server checks on every write, by column name. */
|
|
322
|
+
constraints?: WidgetDatastoreTemplateConstraint[];
|
|
270
323
|
}
|
|
271
324
|
|
|
272
325
|
/**
|
|
@@ -409,6 +462,13 @@ export interface WidgetManifest {
|
|
|
409
462
|
* while the widget's page is closed so its menu badge stays live.
|
|
410
463
|
*/
|
|
411
464
|
badgeCounter?: boolean;
|
|
465
|
+
/**
|
|
466
|
+
* Optional (sc-11348, default false). True when the widget owns the rest of
|
|
467
|
+
* the screen (a messenger, a map): dropping it at the top level of a page
|
|
468
|
+
* turns that section's "Fit to screen" on. Honour `useFill()` and
|
|
469
|
+
* `useFitScreen()` with it.
|
|
470
|
+
*/
|
|
471
|
+
prefersFitToScreen?: boolean;
|
|
412
472
|
}
|
|
413
473
|
|
|
414
474
|
/**
|
|
@@ -733,6 +793,12 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
733
793
|
* Absent / `false` everywhere the host has not opted the widget into filling.
|
|
734
794
|
*/
|
|
735
795
|
fill?: boolean;
|
|
796
|
+
/**
|
|
797
|
+
* sc-11348 — optional host hint backing `useFitScreen()`: `true` when this
|
|
798
|
+
* widget is the surface of a section set to Fit to screen (edge to edge,
|
|
799
|
+
* the rest of the screen). Absent / `false` everywhere else.
|
|
800
|
+
*/
|
|
801
|
+
fitScreen?: boolean;
|
|
736
802
|
/** Active end-user identity from the host-built context (camelCase, not a wire payload). `id` is null when anonymous. */
|
|
737
803
|
user: {
|
|
738
804
|
id: string | null;
|
|
@@ -1048,6 +1114,65 @@ export function useDatastoreFigure(
|
|
|
1048
1114
|
options?: FigureOptions,
|
|
1049
1115
|
): FigureResult;
|
|
1050
1116
|
|
|
1117
|
+
export interface AggregateBucketSpec {
|
|
1118
|
+
/** Bucket a system date column: `created_at` (default) or `updated_at`. */
|
|
1119
|
+
groupBy?: "created_at" | "updated_at";
|
|
1120
|
+
/** UTC days or ISO weeks (Monday); `day` by default. */
|
|
1121
|
+
bucket?: "day" | "week";
|
|
1122
|
+
/** ISO 8601. Defaults to 30 days (or 12 weeks) before `to`. */
|
|
1123
|
+
from?: string;
|
|
1124
|
+
/** ISO 8601. Defaults to now. At most 31 buckets in the range. */
|
|
1125
|
+
to?: string;
|
|
1126
|
+
/** A plain number column to add up per bucket. */
|
|
1127
|
+
sumColumn?: string;
|
|
1128
|
+
filter?: FigureOptions["filter"];
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export interface AggregateGroupSpec {
|
|
1132
|
+
/** A text, number, yes/no, date or user column. */
|
|
1133
|
+
groupBy: string;
|
|
1134
|
+
/** A plain number column to add up per group. */
|
|
1135
|
+
sumColumn?: string;
|
|
1136
|
+
filter?: FigureOptions["filter"];
|
|
1137
|
+
/** Default: by value. `count:desc` with a `limit` ranks the biggest groups. */
|
|
1138
|
+
order?: "count:desc" | "count:asc" | "sum:desc" | "sum:asc" | "group:asc" | "group:desc";
|
|
1139
|
+
/** 1–1000. */
|
|
1140
|
+
limit?: number;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export interface AggregateBucket {
|
|
1144
|
+
/** The bucket's start date, `YYYY-MM-DD`. */
|
|
1145
|
+
date: string;
|
|
1146
|
+
count: number;
|
|
1147
|
+
/** 0 unless `sumColumn` was given. */
|
|
1148
|
+
sum: number;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
export interface AggregateGroup {
|
|
1152
|
+
/** The value; a user column's user id; "Uncategorized" for rows with none. */
|
|
1153
|
+
group: string;
|
|
1154
|
+
count: number;
|
|
1155
|
+
/** 0 unless `sumColumn` was given. */
|
|
1156
|
+
sum: number;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
export interface AggregateResult {
|
|
1160
|
+
/** By date: every bucket of the range, empty ones as `count: 0`. */
|
|
1161
|
+
buckets: AggregateBucket[];
|
|
1162
|
+
/** By a column: one entry per value. */
|
|
1163
|
+
groups: AggregateGroup[];
|
|
1164
|
+
loading: boolean;
|
|
1165
|
+
fetching: boolean;
|
|
1166
|
+
error: Error | null;
|
|
1167
|
+
refetch(): Promise<void>;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/** Rows per day, week or column value over every row the viewer can read, counted on the server. */
|
|
1171
|
+
export function useDatastoreAggregate(
|
|
1172
|
+
table: string | null | undefined,
|
|
1173
|
+
spec?: AggregateBucketSpec | AggregateGroupSpec,
|
|
1174
|
+
): AggregateResult;
|
|
1175
|
+
|
|
1051
1176
|
export interface UserStatsQuery {
|
|
1052
1177
|
days?: 7 | 30 | 90;
|
|
1053
1178
|
/** A `useGroups()` row id: narrows user and invite figures to that group. */
|
|
@@ -1492,6 +1617,15 @@ export interface DatastoreSchemaColumn {
|
|
|
1492
1617
|
display_column_id?: string | null;
|
|
1493
1618
|
/** True when this column is the table's display/identification column. */
|
|
1494
1619
|
is_identification?: boolean;
|
|
1620
|
+
/**
|
|
1621
|
+
* Set on a relation the server keeps at the newest related row; the id of
|
|
1622
|
+
* the related table's relation back. Clients cannot write such a column.
|
|
1623
|
+
*/
|
|
1624
|
+
latest_via_column_id?: string | null;
|
|
1625
|
+
/** Set on a copy the server keeps: the latest-row column it reads through. */
|
|
1626
|
+
copy_via_column_id?: string | null;
|
|
1627
|
+
/** Set on a copy the server keeps: the related table's column it copies. */
|
|
1628
|
+
copy_of_column_id?: string | null;
|
|
1495
1629
|
}
|
|
1496
1630
|
|
|
1497
1631
|
export interface DatastoreSchema {
|
|
@@ -2018,6 +2152,14 @@ export function useAnonymousAccount(): AnonymousAccountApi;
|
|
|
2018
2152
|
*/
|
|
2019
2153
|
export function useFill(): boolean;
|
|
2020
2154
|
|
|
2155
|
+
/**
|
|
2156
|
+
* sc-11348 — returns `true` when this widget is the surface of a page section
|
|
2157
|
+
* set to "Fit to screen": it runs edge to edge and takes the rest of the
|
|
2158
|
+
* screen, so drop your own frame (border, rounded corners, shadow).
|
|
2159
|
+
* `useFill()` is `true` too. Defaults to `false`; the same on web and native.
|
|
2160
|
+
*/
|
|
2161
|
+
export function useFitScreen(): boolean;
|
|
2162
|
+
|
|
2021
2163
|
/**
|
|
2022
2164
|
* The host-provided navigation surface. `goTo(pageId, params?)` navigates
|
|
2023
2165
|
* to an internal app page; `goBack()` pops the stack. `goToStartPage()`
|
package/dist/index.js
CHANGED
|
@@ -80,6 +80,7 @@ export {
|
|
|
80
80
|
useUserPhoto,
|
|
81
81
|
useAnonymousAccount,
|
|
82
82
|
useFill,
|
|
83
|
+
useFitScreen,
|
|
83
84
|
useNavigation,
|
|
84
85
|
useRouteParams,
|
|
85
86
|
usePageBadge,
|
|
@@ -125,6 +126,7 @@ export {
|
|
|
125
126
|
} from "./audio.js";
|
|
126
127
|
export { useDatastoreFigure } from "./datastore-figure.js";
|
|
127
128
|
export {
|
|
129
|
+
useDatastoreAggregate,
|
|
128
130
|
useUserStats,
|
|
129
131
|
useUserActivity,
|
|
130
132
|
useActiveUsers,
|
package/dist/index.native.js
CHANGED
|
@@ -80,6 +80,7 @@ export {
|
|
|
80
80
|
useUserPhoto,
|
|
81
81
|
useAnonymousAccount,
|
|
82
82
|
useFill,
|
|
83
|
+
useFitScreen,
|
|
83
84
|
useNavigation,
|
|
84
85
|
useRouteParams,
|
|
85
86
|
usePageBadge,
|
|
@@ -123,6 +124,7 @@ export {
|
|
|
123
124
|
} from "./audio.js";
|
|
124
125
|
export { useDatastoreFigure } from "./datastore-figure.js";
|
|
125
126
|
export {
|
|
127
|
+
useDatastoreAggregate,
|
|
126
128
|
useUserStats,
|
|
127
129
|
useUserActivity,
|
|
128
130
|
useActiveUsers,
|
package/dist/manifest.cjs
CHANGED
|
@@ -331,6 +331,12 @@ function validateManifest(m) {
|
|
|
331
331
|
"manifest.badgeCounter must be a boolean when present",
|
|
332
332
|
);
|
|
333
333
|
|
|
334
|
+
pushIf(
|
|
335
|
+
errors,
|
|
336
|
+
manifest.prefersFitToScreen === undefined || typeof manifest.prefersFitToScreen === "boolean",
|
|
337
|
+
"manifest.prefersFitToScreen must be a boolean when present",
|
|
338
|
+
);
|
|
339
|
+
|
|
334
340
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
335
341
|
}
|
|
336
342
|
|
package/dist/manifest.js
CHANGED
|
@@ -377,6 +377,12 @@ function validateManifest(m) {
|
|
|
377
377
|
"manifest.badgeCounter must be a boolean when present",
|
|
378
378
|
);
|
|
379
379
|
|
|
380
|
+
pushIf(
|
|
381
|
+
errors,
|
|
382
|
+
manifest.prefersFitToScreen === undefined || typeof manifest.prefersFitToScreen === "boolean",
|
|
383
|
+
"manifest.prefersFitToScreen must be a boolean when present",
|
|
384
|
+
);
|
|
385
|
+
|
|
380
386
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
381
387
|
}
|
|
382
388
|
|
package/dist/stats-hooks.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
// sc-10691 — read-only hooks
|
|
2
|
-
//
|
|
3
|
-
// re-
|
|
1
|
+
// sc-10691 — read-only aggregate hooks: record counts per day, week or column
|
|
2
|
+
// value, and an app's user totals. Both are one keyed read, stale-while-revalidate like
|
|
3
|
+
// useDatastoreQuery, and both re-run on the page's pull-to-refresh.
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
toDatastoreError,
|
|
7
|
+
toDirectoryError,
|
|
8
|
+
useRefresh,
|
|
9
|
+
useWidgetContextOrThrow,
|
|
10
|
+
} from "./hooks.js";
|
|
11
|
+
|
|
12
|
+
// The system date columns bucket by day or week; any other column groups by value.
|
|
13
|
+
const DATE_GROUP_FIELDS = new Set(["created_at", "updated_at"]);
|
|
6
14
|
|
|
7
15
|
const EMPTY = { identity: null, key: null, data: null, error: null };
|
|
8
16
|
|
|
@@ -66,6 +74,54 @@ function useKeyedRead(read, identity, key, mapError) {
|
|
|
66
74
|
};
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Counts (and optionally sums) a table's rows on the server, over every row
|
|
79
|
+
* the viewer may read — not just one page. `spec`: `{ groupBy, sumColumn?,
|
|
80
|
+
* filter? }` plus, by date (`groupBy` "created_at" | "updated_at"), `bucket?:
|
|
81
|
+
* "day" | "week", from?, to?`, or, by a column, `order?` ("count:desc",
|
|
82
|
+
* "count:asc", "sum:desc", "sum:asc", "group:asc", "group:desc") and `limit?`
|
|
83
|
+
* (1–1000). Returns `{ buckets, groups, loading, fetching, error, refetch }`:
|
|
84
|
+
* by date, `buckets` is `[{ date, count, sum }]` with every bucket of the range
|
|
85
|
+
* present (empty ones as 0); by a column, `groups` is `[{ group, count, sum }]`,
|
|
86
|
+
* a user column's `group` being the user id and rows with no value one
|
|
87
|
+
* "Uncategorized" group. A falsy `table` settles to nothing.
|
|
88
|
+
*/
|
|
89
|
+
export function useDatastoreAggregate(table, spec) {
|
|
90
|
+
const ctx = useWidgetContextOrThrow("useDatastoreAggregate");
|
|
91
|
+
if (!ctx.datastore || typeof ctx.datastore.records !== "function") {
|
|
92
|
+
throw new Error("useDatastoreAggregate: host did not inject a datastore client");
|
|
93
|
+
}
|
|
94
|
+
const { groupBy = "created_at", bucket, from, to, sumColumn, filter, order, limit } = spec || {};
|
|
95
|
+
const byDate = DATE_GROUP_FIELDS.has(groupBy);
|
|
96
|
+
const key = table
|
|
97
|
+
? serialize(byDate ? [groupBy, bucket, from, to, sumColumn, filter] : [groupBy, sumColumn, filter, order, limit])
|
|
98
|
+
: null;
|
|
99
|
+
const records = ctx.datastore.records;
|
|
100
|
+
|
|
101
|
+
const result = useKeyedRead(
|
|
102
|
+
async () => {
|
|
103
|
+
const rows = await records(table).aggregate(
|
|
104
|
+
byDate
|
|
105
|
+
? { groupBy, bucket, from, to, sumField: sumColumn, filter }
|
|
106
|
+
: { groupBy, sumField: sumColumn, filter, order, limit },
|
|
107
|
+
);
|
|
108
|
+
return Array.isArray(rows) ? rows : [];
|
|
109
|
+
},
|
|
110
|
+
// The mode is part of the identity: switching it must not show the other's rows.
|
|
111
|
+
table ? `${table}|${byDate ? "date" : "column"}` : null,
|
|
112
|
+
key,
|
|
113
|
+
toDatastoreError,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const { data, ...state } = result;
|
|
117
|
+
const rows = data || [];
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
buckets: byDate ? rows.map((row) => ({ date: row.group, count: row.count, sum: row.sum })) : [],
|
|
121
|
+
groups: byDate ? [] : rows.map((row) => ({ group: row.group, count: row.count, sum: row.sum })),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
69
125
|
/**
|
|
70
126
|
* The app's user totals for its own admins — never a list of people.
|
|
71
127
|
* `query`: `{ days?: 7 | 30 | 90, group_id? }`. Returns `{ stats, loading,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.228.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",
|
package/src/contract.js
CHANGED
|
@@ -991,6 +991,24 @@ const HOOKS = [
|
|
|
991
991
|
requiredContextSlice: ["fill"],
|
|
992
992
|
scopes: null,
|
|
993
993
|
},
|
|
994
|
+
{
|
|
995
|
+
name: "useFitScreen",
|
|
996
|
+
signature: "useFitScreen()",
|
|
997
|
+
description:
|
|
998
|
+
"Returns true when this widget is the surface of a page section set to " +
|
|
999
|
+
"\"Fit to screen\": the section runs edge to edge across the app's " +
|
|
1000
|
+
"content area and takes the rest of the screen, so the widget should " +
|
|
1001
|
+
"drop its own frame (border, rounded corners, shadow) and meet the " +
|
|
1002
|
+
"edges cleanly. useFill() is true as well then, and the host has already " +
|
|
1003
|
+
"removed the widget's outer margin. Defaults to false wherever the host " +
|
|
1004
|
+
"has not fitted the widget, so calling it is always safe. The SAME value " +
|
|
1005
|
+
"is injected by the web Player and the native export.",
|
|
1006
|
+
returnShape: {
|
|
1007
|
+
"(returns)": "boolean",
|
|
1008
|
+
},
|
|
1009
|
+
requiredContextSlice: ["fitScreen"],
|
|
1010
|
+
scopes: null,
|
|
1011
|
+
},
|
|
994
1012
|
{
|
|
995
1013
|
name: "useChildRenderer",
|
|
996
1014
|
signature: "useChildRenderer()",
|
|
@@ -1397,7 +1415,8 @@ const HOOKS = [
|
|
|
1397
1415
|
"create the row, call upload(file, { recordId: row.id }) — the per-call " +
|
|
1398
1416
|
"recordId names the row the hook could not know at render — then write " +
|
|
1399
1417
|
"the returned id into the FILE column with update(). A logged-out " +
|
|
1400
|
-
"visitor
|
|
1418
|
+
"visitor may do this only for a row they just created, within a few " +
|
|
1419
|
+
"minutes, and never edits other rows' files. For a file that belongs to " +
|
|
1401
1420
|
"no row: 'public' for content the app shows to anyone browsing, " +
|
|
1402
1421
|
"'project' for signed-in workspace users only, 'personal' for a file " +
|
|
1403
1422
|
"private to the uploading app user.",
|
|
@@ -1796,6 +1815,45 @@ const HOOKS = [
|
|
|
1796
1815
|
requiredContextSlice: ["datastore.records"],
|
|
1797
1816
|
scopes: ["datastore.read:*"],
|
|
1798
1817
|
},
|
|
1818
|
+
// sc-10942 — rows per day or week, counted on the server over every row the
|
|
1819
|
+
// viewer may read. Date columns only until column grouping is exact too.
|
|
1820
|
+
{
|
|
1821
|
+
name: "useDatastoreAggregate",
|
|
1822
|
+
signature:
|
|
1823
|
+
"useDatastoreAggregate(tableId, { groupBy, bucket, from, to, sumColumn, filter, order, limit })",
|
|
1824
|
+
optionalArgs: ["groupBy", "bucket", "from", "to", "sumColumn", "filter", "order", "limit"],
|
|
1825
|
+
description:
|
|
1826
|
+
"Counts rows on the server over EVERY row the viewer can read, so it is " +
|
|
1827
|
+
"exact past the 1000-row page a list read returns — never group " +
|
|
1828
|
+
"useDatastoreQuery rows by hand. With `sumColumn` it also totals a plain " +
|
|
1829
|
+
"number column. BY DATE (`groupBy` \"created_at\", the default, or " +
|
|
1830
|
+
"\"updated_at\"): a SERIES for a chart, one entry per UTC day or ISO week " +
|
|
1831
|
+
"(Monday) in `buckets`; `bucket` is \"day\" (default) or \"week\"; " +
|
|
1832
|
+
"`from` / `to` are ISO dates (default: the 30 days, or 12 weeks, up to " +
|
|
1833
|
+
"now), at most 31 buckets, empty ones as count 0 so a chart has no gaps. " +
|
|
1834
|
+
"BY A COLUMN (any other `groupBy`: a text, number, yes/no, date or user " +
|
|
1835
|
+
"column): one entry per value in `groups`. A USER column's `group` is the " +
|
|
1836
|
+
"user id (resolve names with useDirectory({ ids })), so `order: \"count:desc\", " +
|
|
1837
|
+
"limit: 10` ranks the ten people with the most rows. `order` is count, sum " +
|
|
1838
|
+
"or group, then :asc or :desc (default: by value); `limit` is 1-1000. Rows " +
|
|
1839
|
+
"with no value form one \"Uncategorized\" group; on a user column, leave " +
|
|
1840
|
+
"them out with a `nempty:` filter. Encrypted, array, file and relation " +
|
|
1841
|
+
"columns cannot be grouped (VALIDATION). `filter` takes the same shapes " +
|
|
1842
|
+
"as useDatastoreQuery's, created_at ranges included. Pass null as the " +
|
|
1843
|
+
"table until a bound table is known.",
|
|
1844
|
+
returnShape: {
|
|
1845
|
+
buckets:
|
|
1846
|
+
"Array<{ date, count, sum }> // date = the bucket's start, YYYY-MM-DD; sum is 0 without sumColumn",
|
|
1847
|
+
groups:
|
|
1848
|
+
"Array<{ group, count, sum }> // by a column; a USER column's group is the user id",
|
|
1849
|
+
loading: "boolean",
|
|
1850
|
+
fetching: "boolean",
|
|
1851
|
+
error: "DatastoreError | null",
|
|
1852
|
+
refetch: "() => Promise<void>",
|
|
1853
|
+
},
|
|
1854
|
+
requiredContextSlice: ["datastore.records"],
|
|
1855
|
+
scopes: ["datastore.read:*"],
|
|
1856
|
+
},
|
|
1799
1857
|
{
|
|
1800
1858
|
name: "useDatastoreSchema",
|
|
1801
1859
|
signature: "useDatastoreSchema(tableId)",
|
|
@@ -3528,6 +3586,13 @@ const MANIFEST_SCHEMA = {
|
|
|
3528
3586
|
"Optional (default false). TRUE when every platform entry also exports useBadgeCount(props): a hook returning the count the widget puts on its page's menu entry with usePageBadge, so the app keeps that badge live while the page is closed. The host runs counters for installed marketplace widgets (the workspace's own published widgets included); the platform's built-in widgets declare none. It calls the hook with the instance's resolved props, inside the widget's own context; the hook returns a whole number, or null while the count is not known yet. It only reads, and the host enforces that: the counter's datastore and directory expose only their read methods (plus realtime subscriptions) and notification mutes only get/list, while writes, typing signals, payments, notifications.send, files, assets, AI agents, translation, link previews, device capabilities, identification and account actions all answer as unavailable. It also renders nothing, navigates nowhere, emits no events or toasts, marks nothing read, and calls none of usePageBadge, useScreenTitle, useSectionEmpty or useWidgetRoute's setter. The linter refuses a declared counter whose entry has no useBadgeCount export. The AppStudio widget agent leaves this unset: a generated widget never declares a badge counter.",
|
|
3529
3587
|
default: false,
|
|
3530
3588
|
},
|
|
3589
|
+
prefersFitToScreen: {
|
|
3590
|
+
type: "boolean",
|
|
3591
|
+
required: false,
|
|
3592
|
+
description:
|
|
3593
|
+
"Optional (default false). TRUE when the widget is built to own the rest of the screen: it scrolls its own content and keeps its controls in view, like a messenger, a map or a full-height board. Dropping the widget at the top level of a page in the Builder turns that section's \"Fit to screen\" setting on (the maker can switch it off, and it stays off), and the app-builder fits the section it plans the widget into. A fit section runs edge to edge across the content area and takes the rest of the screen; the widget learns it through useFill() and useFitScreen(), and the host gives it a box exactly the height left, which it scrolls inside (a widget without the field grows with its content there, and the page scrolls). Set it only for a widget that honours useFill() and scrolls inside the height it is given; leave it false (or omit) for everything else.",
|
|
3594
|
+
default: false,
|
|
3595
|
+
},
|
|
3531
3596
|
events: {
|
|
3532
3597
|
type: "object[]",
|
|
3533
3598
|
required: true,
|
|
@@ -3559,7 +3624,7 @@ const MANIFEST_SCHEMA = {
|
|
|
3559
3624
|
type: "object",
|
|
3560
3625
|
required: false,
|
|
3561
3626
|
description:
|
|
3562
|
-
"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. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table.",
|
|
3627
|
+
"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. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
3563
3628
|
},
|
|
3564
3629
|
translations: {
|
|
3565
3630
|
type: "object",
|
|
@@ -3603,6 +3668,16 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
3603
3668
|
required: false,
|
|
3604
3669
|
fields: {},
|
|
3605
3670
|
},
|
|
3671
|
+
// sc-11348 — optional host hint backing useFitScreen(): the widget is the
|
|
3672
|
+
// surface of a section set to Fit to screen, so it drops its own frame.
|
|
3673
|
+
fitScreen: {
|
|
3674
|
+
description:
|
|
3675
|
+
"Optional host layout hint (boolean). True when this widget is the " +
|
|
3676
|
+
"surface of a page section set to Fit to screen: edge to edge, the rest " +
|
|
3677
|
+
"of the screen. Backs useFitScreen(); defaults to false when absent.",
|
|
3678
|
+
required: false,
|
|
3679
|
+
fields: {},
|
|
3680
|
+
},
|
|
3606
3681
|
user: {
|
|
3607
3682
|
description:
|
|
3608
3683
|
"Signed-in user, host-built context object (camelCase: { id, email, displayName, photoUrl, roles, groupIds }) — NOT a wire payload, so it is the one camelCase island among the hooks. photoUrl is the user's own profile photo as an absolute URL, or null. email is null for an account created without one. Not a data-client.",
|
|
@@ -5878,7 +5953,28 @@ const CONTRACT = deepFreeze({
|
|
|
5878
5953
|
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5879
5954
|
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5880
5955
|
// the signed-in user's own start page on both hosts.
|
|
5881
|
-
|
|
5956
|
+
// 1.180.0: additive (sc-11244) — a logged-out visitor may attach files to
|
|
5957
|
+
// the row they just created, for a few minutes: the host carries a grant
|
|
5958
|
+
// from the create to that row's upload and update. Widget code is the
|
|
5959
|
+
// same create → upload → update flow.
|
|
5960
|
+
// 1.181.0: additive (sc-10942) — `useDatastoreAggregate(tableId, spec)`
|
|
5961
|
+
// counts rows per day or week on the server (date columns only), through
|
|
5962
|
+
// `records(t).aggregate`'s new bucket/from/to.
|
|
5963
|
+
// 1.182.0: additive (sc-10737) — `useDatastoreAggregate` groups by an
|
|
5964
|
+
// ordinary column too, into `groups` ([{ group, count, sum }]), with
|
|
5965
|
+
// `order` and `limit` for a top N; counted on the server over every
|
|
5966
|
+
// readable row. A user column groups by user id.
|
|
5967
|
+
// 1.183.0: additive (sc-10766) — a datastoreTemplate column may declare
|
|
5968
|
+
// `latestVia` (a server-kept pointer to the newest related row) or
|
|
5969
|
+
// `copyOf: { via, column }` (a server-kept copy read through one), and
|
|
5970
|
+
// a table may declare `constraints` (`unique` with `unlessRelated`,
|
|
5971
|
+
// `no-overlap`, `related-open`, `related-index`).
|
|
5972
|
+
// 1.184.0: additive (sc-11348) — OPTIONAL manifest field
|
|
5973
|
+
// `prefersFitToScreen` (dropping the widget at a page's top level turns
|
|
5974
|
+
// that section's Fit to screen on), and `useFitScreen()` with its
|
|
5975
|
+
// optional `fitScreen` host hint: the widget is a fit section's surface,
|
|
5976
|
+
// so it drops its own frame. Existing manifests and hosts read false.
|
|
5977
|
+
version: "1.184.0",
|
|
5882
5978
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5883
5979
|
hooks: HOOKS,
|
|
5884
5980
|
primitives: PRIMITIVES,
|