@colixsystems/widget-sdk 0.18.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -29
- package/dist/contract.cjs +228 -113
- package/dist/contract.js +238 -122
- package/dist/hooks.js +781 -570
- package/dist/index.d.ts +257 -65
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/dist/linter.cjs +56 -0
- package/dist/linter.js +57 -0
- package/dist/manifest.cjs +157 -2
- package/dist/manifest.js +157 -2
- package/package.json +2 -2
package/dist/contract.cjs
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
const DEFAULT_THEME_TOKENS = Object.freeze({
|
|
12
12
|
colors: Object.freeze({
|
|
13
|
-
primary: "#
|
|
13
|
+
primary: "#3b82f6",
|
|
14
14
|
onPrimary: "#ffffff",
|
|
15
|
-
secondary: "#
|
|
15
|
+
secondary: "#10b981",
|
|
16
16
|
onSecondary: "#ffffff",
|
|
17
17
|
surface: "#ffffff",
|
|
18
18
|
onSurface: "#111827",
|
|
@@ -70,6 +70,26 @@ const HOOKS = [
|
|
|
70
70
|
requiredContextSlice: ["user"],
|
|
71
71
|
scopes: null,
|
|
72
72
|
},
|
|
73
|
+
{
|
|
74
|
+
name: "useFill",
|
|
75
|
+
signature: "useFill()",
|
|
76
|
+
description:
|
|
77
|
+
"Returns true when the host has sized this widget to fill its layout " +
|
|
78
|
+
'slot\'s available height — a page-grid tile whose author chose "Fill ' +
|
|
79
|
+
'tile height", or a widget type that fills by default (containers and ' +
|
|
80
|
+
"media). When true, a widget that has a meaningful filled form (Image, " +
|
|
81
|
+
"Chart, Map, Video) should switch from its intrinsic height to a stretch " +
|
|
82
|
+
'style (flex: 1 / height: "100%"); widgets with no useful filled form ' +
|
|
83
|
+
"may ignore it. Defaults to false wherever the host has not opted the " +
|
|
84
|
+
"widget into filling, so calling it is always safe. The SAME value is " +
|
|
85
|
+
"injected by the web Player and the native export (CLAUDE.md §3), so a " +
|
|
86
|
+
"widget's fill behaviour is identical on both platforms.",
|
|
87
|
+
returnShape: {
|
|
88
|
+
"(returns)": "boolean",
|
|
89
|
+
},
|
|
90
|
+
requiredContextSlice: ["fill"],
|
|
91
|
+
scopes: null,
|
|
92
|
+
},
|
|
73
93
|
{
|
|
74
94
|
name: "useChildRenderer",
|
|
75
95
|
signature: "useChildRenderer()",
|
|
@@ -167,12 +187,12 @@ const HOOKS = [
|
|
|
167
187
|
name: "useDirectory",
|
|
168
188
|
signature: "useDirectory(query?)",
|
|
169
189
|
returnShape: {
|
|
170
|
-
users: "Array<{ id, name, role }>",
|
|
190
|
+
users: "Array<{ id, name, role }> // snake_case rows; unwrapped from { data, meta }",
|
|
171
191
|
loading: "boolean",
|
|
172
192
|
error: "DatastoreError | null",
|
|
173
193
|
refetch: "() => Promise<void>",
|
|
174
194
|
},
|
|
175
|
-
requiredContextSlice: ["directory.
|
|
195
|
+
requiredContextSlice: ["directory.users"],
|
|
176
196
|
scopes: ["directory.read:users"],
|
|
177
197
|
},
|
|
178
198
|
{
|
|
@@ -206,28 +226,26 @@ const HOOKS = [
|
|
|
206
226
|
name: "useUsers",
|
|
207
227
|
signature: "useUsers(query?)",
|
|
208
228
|
description:
|
|
209
|
-
"AppUser administration
|
|
210
|
-
"deactivate,
|
|
211
|
-
"
|
|
212
|
-
"
|
|
229
|
+
"AppUser administration via the injected directory-client at " +
|
|
230
|
+
"ctx.directory.users.{list,get,invite,deactivate,reactivate}. Returns " +
|
|
231
|
+
"{ users, loading, error, refetch, invite, deactivate, reactivate, remove }. " +
|
|
232
|
+
"list returns the { data, meta } envelope verbatim — the hook unwraps " +
|
|
233
|
+
"res.data; rows are snake_case (is_active, …). Reads need users.read:* " +
|
|
234
|
+
"scope; mutations need users.write:*. The `invite` call accepts " +
|
|
235
|
+
"{ email, name, group_ids? } and returns the resulting AppUserInvite row " +
|
|
236
|
+
"(the email is sent by the host).",
|
|
213
237
|
returnShape: {
|
|
214
|
-
users: "Array<{ id, name, email?, role,
|
|
238
|
+
users: "Array<{ id, name, email?, role, is_active }> // snake_case rows; unwrapped from { data, meta }",
|
|
215
239
|
loading: "boolean",
|
|
216
240
|
error: "DirectoryError | null",
|
|
217
241
|
refetch: "() => Promise<void>",
|
|
218
242
|
invite:
|
|
219
|
-
"({ email, name,
|
|
243
|
+
"({ email, name, group_ids? }) => Promise<Invite> // rejects with DirectoryError",
|
|
220
244
|
deactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
221
245
|
reactivate: "(userId) => Promise<User> // rejects with DirectoryError",
|
|
222
246
|
remove: "(userId) => Promise<void> // rejects with DirectoryError",
|
|
223
247
|
},
|
|
224
|
-
requiredContextSlice: [
|
|
225
|
-
"users.listUsers",
|
|
226
|
-
"users.invite",
|
|
227
|
-
"users.deactivate",
|
|
228
|
-
"users.reactivate",
|
|
229
|
-
"users.remove",
|
|
230
|
-
],
|
|
248
|
+
requiredContextSlice: ["directory.users"],
|
|
231
249
|
scopes: ["users.read:*"],
|
|
232
250
|
},
|
|
233
251
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration. Returns
|
|
@@ -239,11 +257,14 @@ const HOOKS = [
|
|
|
239
257
|
name: "useGroups",
|
|
240
258
|
signature: "useGroups(query?)",
|
|
241
259
|
description:
|
|
242
|
-
"AppUserGroup administration
|
|
243
|
-
"create,
|
|
260
|
+
"AppUserGroup administration via the injected directory-client at " +
|
|
261
|
+
"ctx.directory.groups.{list,create,remove,addMember,removeMember,listMine}. " +
|
|
262
|
+
"Returns { groups, loading, error, refetch, create, remove, addMember, " +
|
|
263
|
+
"removeMember }. list returns the { data, meta } envelope verbatim — the " +
|
|
264
|
+
"hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
|
|
244
265
|
"mutations need groups.write:*.",
|
|
245
266
|
returnShape: {
|
|
246
|
-
groups: "Array<{ id, name,
|
|
267
|
+
groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
|
|
247
268
|
loading: "boolean",
|
|
248
269
|
error: "DirectoryError | null",
|
|
249
270
|
refetch: "() => Promise<void>",
|
|
@@ -255,13 +276,7 @@ const HOOKS = [
|
|
|
255
276
|
removeMember:
|
|
256
277
|
"(groupId, userId) => Promise<void> // rejects with DirectoryError",
|
|
257
278
|
},
|
|
258
|
-
requiredContextSlice: [
|
|
259
|
-
"groups.listGroups",
|
|
260
|
-
"groups.create",
|
|
261
|
-
"groups.remove",
|
|
262
|
-
"groups.addMember",
|
|
263
|
-
"groups.removeMember",
|
|
264
|
-
],
|
|
279
|
+
requiredContextSlice: ["directory.groups"],
|
|
265
280
|
scopes: ["groups.read:*"],
|
|
266
281
|
},
|
|
267
282
|
// REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
@@ -270,33 +285,33 @@ const HOOKS = [
|
|
|
270
285
|
name: "useRecordPermissions",
|
|
271
286
|
signature: "useRecordPermissions(tableId, recordId)",
|
|
272
287
|
description:
|
|
273
|
-
"Manage per-record VirtualPermission grants on a single record
|
|
274
|
-
"
|
|
275
|
-
"permissions
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"(
|
|
279
|
-
"
|
|
280
|
-
"
|
|
288
|
+
"Manage per-record VirtualPermission grants on a single record via the " +
|
|
289
|
+
"injected datastore-client at " +
|
|
290
|
+
"ctx.datastore.records(tableId).permissions(recordId).{list,grant,update,revoke}. " +
|
|
291
|
+
"Returns { permissions, loading, error, grant, revoke, update, refetch } " +
|
|
292
|
+
"where permissions is Array<{ id, user_id, group_id, can_read, can_write, " +
|
|
293
|
+
"can_delete, can_grant }> (snake_case rows verbatim; list() returns the " +
|
|
294
|
+
"{ data, meta } envelope and the hook unwraps res.data). grant/update " +
|
|
295
|
+
"bodies are snake_case verbatim ({ user_id | group_id, can_read, " +
|
|
296
|
+
"can_write, can_delete, can_grant }). Mutating requires acl.write:records " +
|
|
297
|
+
"scope AND can_grant on the target record (REQ-ACL-RELINHERIT-05: " +
|
|
298
|
+
"APP_USER actors with can_grant are accepted, not only Studio owners). " +
|
|
299
|
+
"When tableId or recordId is null/empty the hook collapses to an empty " +
|
|
300
|
+
"no-op result without a network round-trip.",
|
|
281
301
|
returnShape: {
|
|
282
302
|
permissions:
|
|
283
|
-
"Array<{ id,
|
|
303
|
+
"Array<{ id, user_id, group_id, can_read, can_write, can_delete, can_grant }> // snake_case rows; unwrapped from { data, meta }",
|
|
284
304
|
loading: "boolean",
|
|
285
305
|
error: "PermissionError | null",
|
|
286
306
|
grant:
|
|
287
|
-
"({
|
|
307
|
+
"({ user_id?, group_id?, can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
|
|
288
308
|
revoke:
|
|
289
309
|
"(permissionId) => Promise<void> // rejects with PermissionError",
|
|
290
310
|
update:
|
|
291
|
-
"(permissionId, {
|
|
311
|
+
"(permissionId, { can_read?, can_write?, can_delete?, can_grant? }) => Promise<RecordPermission> // rejects with PermissionError",
|
|
292
312
|
refetch: "() => Promise<void>",
|
|
293
313
|
},
|
|
294
|
-
requiredContextSlice: [
|
|
295
|
-
"recordPermissions.list",
|
|
296
|
-
"recordPermissions.grant",
|
|
297
|
-
"recordPermissions.revoke",
|
|
298
|
-
"recordPermissions.update",
|
|
299
|
-
],
|
|
314
|
+
requiredContextSlice: ["datastore.records"],
|
|
300
315
|
scopes: ["acl.write:records"],
|
|
301
316
|
},
|
|
302
317
|
// REQ-WSDK-PLATFORM §6 — Tier A SDK hooks. Mirror of contract.js.
|
|
@@ -454,10 +469,40 @@ const CATEGORIES = [
|
|
|
454
469
|
"DATA",
|
|
455
470
|
"MEDIA",
|
|
456
471
|
"COMMUNICATION",
|
|
472
|
+
// REQ-USERMGMT-06: app-administration widgets (User Management, …) the
|
|
473
|
+
// published app embeds for its own member management — its own palette
|
|
474
|
+
// section, distinct from COMMUNICATION.
|
|
475
|
+
"ADMINISTRATION",
|
|
457
476
|
"CUSTOM",
|
|
458
477
|
];
|
|
459
478
|
const PLATFORMS = ["web", "native"];
|
|
460
479
|
|
|
480
|
+
// REQ-WIDGET-ACTION — server-side actions a widget may declare in its
|
|
481
|
+
// manifest. Each runs in the shared isolated-vm action runner (see backend
|
|
482
|
+
// action-runner.service.js) on a cron schedule or in response to a record
|
|
483
|
+
// CRUD event — NEVER in the rendered app, so they never affect Player ↔
|
|
484
|
+
// export parity. The trigger vocabulary mirrors the backend Action model.
|
|
485
|
+
const ACTION_TRIGGER_TYPES = [
|
|
486
|
+
"schedule",
|
|
487
|
+
"record_created",
|
|
488
|
+
"record_updated",
|
|
489
|
+
"record_deleted",
|
|
490
|
+
];
|
|
491
|
+
// Globals the action script runs against (the runner's surface) — distinct
|
|
492
|
+
// from the React/SDK widget surface, so the component import/banned-API
|
|
493
|
+
// linter does NOT scan action scripts.
|
|
494
|
+
const ACTION_SCRIPT_GLOBALS = [
|
|
495
|
+
"datastore",
|
|
496
|
+
"fetch",
|
|
497
|
+
"console",
|
|
498
|
+
"record",
|
|
499
|
+
"tenantId",
|
|
500
|
+
"triggerType",
|
|
501
|
+
"triggerTableId",
|
|
502
|
+
];
|
|
503
|
+
// Mirrors action.service.js SCRIPT_MAX_BYTES.
|
|
504
|
+
const ACTION_SCRIPT_MAX_BYTES = 200 * 1024;
|
|
505
|
+
|
|
461
506
|
// Reverse-DNS-ish manifest id, e.g. "com.acme.charts.barchart". Two or
|
|
462
507
|
// more labels, lowercase alnum + hyphen, label starts with a letter. The
|
|
463
508
|
// analyzer + the SDK validator both read this from the contract so a
|
|
@@ -543,6 +588,24 @@ const MANIFEST_SCHEMA = {
|
|
|
543
588
|
description:
|
|
544
589
|
"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` (must be declared earlier in the array). Tables persist across uninstalls.",
|
|
545
590
|
},
|
|
591
|
+
actions: {
|
|
592
|
+
type: "object[]",
|
|
593
|
+
required: false,
|
|
594
|
+
description:
|
|
595
|
+
"Optional. Server-side actions the widget declares. Each runs in the shared isolated-vm action runner (cron- or record-triggered) — NEVER in the rendered app. Operators enable them per tenant from the Properties Panel; the action materialises DISABLED until they bind an integration API key (and, for record_* triggers, a target table) in the Actions admin page. Each entry: { key (stable, unique within the manifest), name, description?, triggerType (one of " +
|
|
596
|
+
ACTION_TRIGGER_TYPES.join(", ") +
|
|
597
|
+
"), scheduleCron? (required iff triggerType=='schedule'; node-cron syntax), timeoutMs? (100–300000), scriptSource (≤200 KiB; runs against " +
|
|
598
|
+
ACTION_SCRIPT_GLOBALS.join(", ") +
|
|
599
|
+
" — NOT the React surface, so SDK imports/hooks are unavailable) }. Do NOT include triggerTableId or apiKeyId — those are tenant-local and bound after install.",
|
|
600
|
+
default: [],
|
|
601
|
+
},
|
|
602
|
+
translations: {
|
|
603
|
+
type: "object",
|
|
604
|
+
required: false,
|
|
605
|
+
description:
|
|
606
|
+
"Optional (REQ-L10N-WIDGET). Translation strings the widget ships. Shape { <key>: { en: string, <locale>?: string, ... } } — `en` is REQUIRED per key. At install the host merges these into the tenant's localization dictionary under a per-widget namespace (`widget.<manifest.id>.<key>`); `useI18n().t(\"<key>\")` resolves the namespaced key automatically, so authors never type the prefix. Non-destructive: an existing admin-edited value is never overwritten, and a language the tenant has not added is never created (values for absent locales are dropped; the widget's `en` seeds the tenant's base language so every key renders). Keys persist across uninstalls; admins prune them from the Translations admin (bulk delete by `widget.<id>.` prefix). Limits: ≤100 keys; each key matches /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/; each value ≤1 KB; the namespaced key must fit the dictionary key cap (128 chars).",
|
|
607
|
+
default: {},
|
|
608
|
+
},
|
|
546
609
|
};
|
|
547
610
|
|
|
548
611
|
const WIDGET_CONTEXT_SHAPE = {
|
|
@@ -558,10 +621,24 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
558
621
|
required: true,
|
|
559
622
|
fields: { id: "manifest.id", version: "manifest.version" },
|
|
560
623
|
},
|
|
624
|
+
// REQ-LAY-08 — optional host layout hint backing useFill(). True when the
|
|
625
|
+
// host sized this widget to fill its layout slot's available height (a
|
|
626
|
+
// page-grid tile set to "Fill tile height", or a default-fill widget type).
|
|
627
|
+
// Optional: a host that never fills a widget simply omits it and useFill()
|
|
628
|
+
// returns false, so existing hosts need no change.
|
|
629
|
+
fill: {
|
|
630
|
+
description:
|
|
631
|
+
"Optional host layout hint (boolean). True when the host sized this " +
|
|
632
|
+
"widget to fill its layout slot's available height (page-grid tile " +
|
|
633
|
+
"fill). Backs useFill(); defaults to false when absent.",
|
|
634
|
+
required: false,
|
|
635
|
+
fields: {},
|
|
636
|
+
},
|
|
561
637
|
user: {
|
|
562
|
-
description:
|
|
638
|
+
description:
|
|
639
|
+
"Signed-in user, host-provided VERBATIM (snake_case: { id, email, display_name, roles, group_ids }). Not a data-client.",
|
|
563
640
|
required: true,
|
|
564
|
-
fields: { id: "string", email: "string",
|
|
641
|
+
fields: { id: "string", email: "string", display_name: "string" },
|
|
565
642
|
},
|
|
566
643
|
workspace: {
|
|
567
644
|
description:
|
|
@@ -581,19 +658,30 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
581
658
|
},
|
|
582
659
|
datastore: {
|
|
583
660
|
description:
|
|
584
|
-
"
|
|
661
|
+
"Injected @colixsystems/datastore-client instance. " +
|
|
662
|
+
"{ tables: { list(), get(idOrName) }, schema(tableId) -> Promise<{ id, name, columns: [...] }>, " +
|
|
663
|
+
"records(tableId) -> { list(query) -> Promise<{ data, meta }>, get(id), create(values), update(id, values), delete(id), aggregate(spec), " +
|
|
664
|
+
"permissions(recordId) -> { list() -> Promise<{ data, meta }>, grant(body), update(permId, patch), revoke(permId) } } }. " +
|
|
665
|
+
"`records` backs the query/record/mutation hooks; `records(t).permissions(r)` backs useRecordPermissions(); `schema` backs useDatastoreSchema(). " +
|
|
666
|
+
"List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case (author column values keep their author-given names).",
|
|
585
667
|
required: true,
|
|
586
|
-
fields: { records: "function", schema: "function" },
|
|
668
|
+
fields: { records: "function", schema: "function", tables: "object" },
|
|
587
669
|
},
|
|
588
670
|
directory: {
|
|
589
671
|
description:
|
|
590
|
-
"
|
|
672
|
+
"Injected @colixsystems/directory-client instance. " +
|
|
673
|
+
"{ me(), users: { list(query?) -> Promise<{ data, meta }>, get(id), invite(body), deactivate(id), reactivate(id) }, " +
|
|
674
|
+
"groups: { list(query?) -> Promise<{ data, meta }>, create(body), remove(id), addMember(groupId, userId), removeMember(groupId, userId), listMine() }, " +
|
|
675
|
+
"invites: { list(), revoke(id), resend(id) } }. " +
|
|
676
|
+
"users backs useDirectory() + useUsers(); groups backs useGroups(). List methods return the { data, meta } envelope verbatim (hooks unwrap res.data); rows/bodies are snake_case. Reads gated by directory.read:users / users.read:* / groups.read:*; mutations by users.write:* / groups.write:*.",
|
|
591
677
|
required: true,
|
|
592
|
-
fields: {
|
|
678
|
+
fields: { users: "object", groups: "object" },
|
|
593
679
|
},
|
|
594
680
|
files: {
|
|
595
681
|
description:
|
|
596
|
-
"
|
|
682
|
+
"Injected @colixsystems/files-client instance, FLATTENED so file ops are top-level. " +
|
|
683
|
+
"{ get(id) -> Promise<{ id, url, ... }>, list(query) -> Promise<{ data, meta }>, upload(formData), folders: { list, create }, shares: { list, create, remove } }. " +
|
|
684
|
+
"Backs useFile(); the returned file already carries an absolute url the widget can drop into an <Image source>.",
|
|
597
685
|
required: true,
|
|
598
686
|
fields: { get: "function" },
|
|
599
687
|
},
|
|
@@ -610,62 +698,17 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
610
698
|
},
|
|
611
699
|
payments: {
|
|
612
700
|
description:
|
|
613
|
-
"
|
|
701
|
+
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status, checkoutUrl? }>, getPayment(id) -> Promise<payment> }. Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
|
|
614
702
|
required: true,
|
|
615
703
|
fields: { requestPayment: "function", getPayment: "function" },
|
|
616
704
|
},
|
|
617
|
-
// REQ-
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
"AppUser administration. { listUsers(query?) -> Promise<User[]>, invite({ email, name, groupIds? }) -> Promise<Invite>, deactivate(userId) -> Promise<User>, reactivate(userId) -> Promise<User>, remove(userId) -> Promise<void> }. Backs useUsers(); reads require users.read:*, mutations require users.write:*.",
|
|
625
|
-
required: true,
|
|
626
|
-
fields: {
|
|
627
|
-
listUsers: "function",
|
|
628
|
-
invite: "function",
|
|
629
|
-
deactivate: "function",
|
|
630
|
-
reactivate: "function",
|
|
631
|
-
remove: "function",
|
|
632
|
-
},
|
|
633
|
-
},
|
|
634
|
-
// REQ-ACL-06 / REQ-ACL-RELINHERIT-05 — per-record VirtualPermission
|
|
635
|
-
// management facade backing useRecordPermissions(). Mirror of
|
|
636
|
-
// contract.js.
|
|
637
|
-
recordPermissions: {
|
|
638
|
-
description:
|
|
639
|
-
"Per-record VirtualPermission management. " +
|
|
640
|
-
"{ list(tableId, recordId) -> Promise<RecordPermission[]>, " +
|
|
641
|
-
"grant(tableId, recordId, body) -> Promise<RecordPermission>, " +
|
|
642
|
-
"revoke(tableId, recordId, permissionId) -> Promise<void>, " +
|
|
643
|
-
"update(tableId, recordId, permissionId, body) -> Promise<RecordPermission> }. " +
|
|
644
|
-
"Backs useRecordPermissions(); requires acl.write:records scope AND " +
|
|
645
|
-
"canGrant on the target record.",
|
|
646
|
-
required: true,
|
|
647
|
-
fields: {
|
|
648
|
-
list: "function",
|
|
649
|
-
grant: "function",
|
|
650
|
-
revoke: "function",
|
|
651
|
-
update: "function",
|
|
652
|
-
},
|
|
653
|
-
},
|
|
654
|
-
// REQ-USERMGMT / REQ-ACL-SYS M3 — AppUserGroup administration facade
|
|
655
|
-
// backing useGroups(). Reads gated by `groups.read:*`; mutations by
|
|
656
|
-
// `groups.write:*`. Same X-Widget-Scopes + SystemAcl gating as users.
|
|
657
|
-
groups: {
|
|
658
|
-
description:
|
|
659
|
-
"AppUserGroup administration. { listGroups(query?) -> Promise<Group[]>, create({ name }) -> Promise<Group>, remove(groupId) -> Promise<void>, addMember(groupId, userId) -> Promise<void>, removeMember(groupId, userId) -> Promise<void> }. Backs useGroups(); reads require groups.read:*, mutations require groups.write:*.",
|
|
660
|
-
required: true,
|
|
661
|
-
fields: {
|
|
662
|
-
listGroups: "function",
|
|
663
|
-
create: "function",
|
|
664
|
-
remove: "function",
|
|
665
|
-
addMember: "function",
|
|
666
|
-
removeMember: "function",
|
|
667
|
-
},
|
|
668
|
-
},
|
|
705
|
+
// REQ-WSDK-DOMAIN-CLIENTS — the AppUser administration, AppUserGroup
|
|
706
|
+
// administration, and per-record VirtualPermission facades that used to
|
|
707
|
+
// live here (`users`, `groups`, `recordPermissions`) were folded into the
|
|
708
|
+
// injected domain-client instances: useUsers()/useGroups() read
|
|
709
|
+
// ctx.directory.{users,groups}; useRecordPermissions() reads
|
|
710
|
+
// ctx.datastore.records(table).permissions(record). See the `directory`
|
|
711
|
+
// and `datastore` slices above.
|
|
669
712
|
i18n: {
|
|
670
713
|
description: "{ t(key, fallback?), locale }.",
|
|
671
714
|
required: true,
|
|
@@ -898,6 +941,20 @@ function deepFreeze(value) {
|
|
|
898
941
|
return Object.freeze(value);
|
|
899
942
|
}
|
|
900
943
|
|
|
944
|
+
// REQ-L10N-WIDGET — a widget's manifest `translations` are merged into the
|
|
945
|
+
// tenant dictionary under a per-widget namespace so they never collide with
|
|
946
|
+
// app keys or another widget's keys. The prefix is DERIVED from the widget id
|
|
947
|
+
// (it "follows the widget"), so authors call `t("greeting")` and the host
|
|
948
|
+
// resolves `widget.<id>.greeting`. This is the ONE definition of the key
|
|
949
|
+
// format: the SDK `useI18n` hook (lookup) and the backend seeder (write) both
|
|
950
|
+
// call it, so the wire key and the lookup key can never drift.
|
|
951
|
+
function widgetTranslationPrefix(id) {
|
|
952
|
+
return `widget.${id}.`;
|
|
953
|
+
}
|
|
954
|
+
function widgetTranslationKey(id, key) {
|
|
955
|
+
return `widget.${id}.${key}`;
|
|
956
|
+
}
|
|
957
|
+
|
|
901
958
|
const CONTRACT = deepFreeze({
|
|
902
959
|
// REQ-WSDK-PLATFORM bump:
|
|
903
960
|
// - `vettedImports` is a new field (rich allowlist with platforms +
|
|
@@ -921,19 +978,71 @@ const CONTRACT = deepFreeze({
|
|
|
921
978
|
// runtime (Form Builder renders inputs by column type). Reads the existing
|
|
922
979
|
// ACL-gated `GET /tables/:id` — structure only, no row data.
|
|
923
980
|
//
|
|
924
|
-
// 1.8.0: additive
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
|
|
981
|
+
// 1.8.0: two additive features.
|
|
982
|
+
// - REQ-WIDGET-ACTION — manifests may declare an optional `actions` array.
|
|
983
|
+
// Each entry is a server-side action (cron- or record-triggered JS) the
|
|
984
|
+
// operator enables per tenant; it runs in the existing isolated-vm action
|
|
985
|
+
// runner, never in the rendered app. New contract fields
|
|
986
|
+
// `actionTriggerTypes`, `actionScriptGlobals`, and `actionScriptMaxBytes`
|
|
987
|
+
// let the docs + agent prompt + validator derive the grammar from one
|
|
988
|
+
// source. New `ADMINISTRATION` manifest category (REQ-USERMGMT-06).
|
|
989
|
+
// - REQ-ACL-06 — new `useRecordPermissions(tableId, recordId)` hook + the
|
|
990
|
+
// `recordPermissions` host-context slice it reads + the `acl.write:records`
|
|
991
|
+
// scope it gates on. Hits the existing REQ-ACL-06
|
|
992
|
+
// /api/v1/tables/:tableId/records/:recordId/permissions REST surface; the
|
|
993
|
+
// host normalises the wire shape into a single principalType+principalId
|
|
994
|
+
// pair widgets branch on. Caller still needs canGrant on the target
|
|
995
|
+
// record (REQ-ACL-RELINHERIT-05).
|
|
996
|
+
//
|
|
997
|
+
// 1.9.0: host-contract change (REQ-WSDK-DOMAIN-CLIENTS) — the host now
|
|
998
|
+
// injects domain-client INSTANCES on the WidgetContext rather than
|
|
999
|
+
// bespoke per-hook facades. ctx.datastore is a @colixsystems/datastore-client
|
|
1000
|
+
// (records(t).list now returns the { data, meta } envelope verbatim;
|
|
1001
|
+
// records(t).permissions(r) replaces the deleted ctx.recordPermissions),
|
|
1002
|
+
// ctx.directory is a @colixsystems/directory-client (users/groups
|
|
1003
|
+
// namespaces replace the deleted ctx.users / ctx.groups; list returns
|
|
1004
|
+
// envelopes), ctx.files is a flattened @colixsystems/files-client,
|
|
1005
|
+
// ctx.payments is a @colixsystems/payments-client. All rows/bodies are
|
|
1006
|
+
// snake_case. Hooks now unwrap the list envelope (res.data ?? []). The
|
|
1007
|
+
// SDK stays duck-typed — it imports none of the four data SDKs. Minor
|
|
1008
|
+
// bump on the contract's pre-1.0 versioning (the breaking channel).
|
|
1009
|
+
//
|
|
1010
|
+
// 1.10.0: additive (REQ-L10N-WIDGET) — manifests may declare an optional
|
|
1011
|
+
// `translations` map ({ <key>: { en, <locale>? } }, en required). The
|
|
1012
|
+
// host seeds them into the tenant l10n dictionary under a per-widget
|
|
1013
|
+
// namespace at install; `useI18n().t(key)` now auto-prefixes the lookup
|
|
1014
|
+
// with `widget.<ctx.widget.id>.` (then falls back to the raw key, so
|
|
1015
|
+
// existing widgets and shared app keys keep resolving). New exported
|
|
1016
|
+
// helpers widgetTranslationPrefix / widgetTranslationKey are the single
|
|
1017
|
+
// source of the key format, shared by the hook and the backend seeder.
|
|
1018
|
+
//
|
|
1019
|
+
// 1.11.0: additive (REQ-LAY-08) — new useFill() hook + the optional `fill`
|
|
1020
|
+
// host-context slice it reads. The host sets ctx.fill=true when it has
|
|
1021
|
+
// sized a widget to fill its layout slot's available height (a page-grid
|
|
1022
|
+
// tile whose author chose "Fill tile height", or a default-fill widget
|
|
1023
|
+
// type — containers + media). Widgets that can stretch (Image, Chart,
|
|
1024
|
+
// Map, Video) switch to flex:1/height:"100%" when useFill() is true; all
|
|
1025
|
+
// others ignore it. The slice is OPTIONAL (defaults false) so existing
|
|
1026
|
+
// hosts need no change, and the same value is injected on web and native
|
|
1027
|
+
// so fill behaviour cannot diverge between the Player and the export.
|
|
1028
|
+
//
|
|
1029
|
+
// 1.11.1: fix — the canonical default `themeTokens.colors` now match the
|
|
1030
|
+
// product's advertised default brand (primary #3b82f6 blue, secondary
|
|
1031
|
+
// #10b981 green) instead of the stale coral/slate that no other surface
|
|
1032
|
+
// used. The Theme Settings tab and Player chrome already defaulted to
|
|
1033
|
+
// these values, so a tenant that never customised its theme rendered
|
|
1034
|
+
// widgets in coral until a first save persisted the blue — a divergence
|
|
1035
|
+
// between the unsaved and saved-defaults render. Aligning the canonical
|
|
1036
|
+
// tokens removes it (no shape/signature change — default-value fix only).
|
|
1037
|
+
version: "1.11.1",
|
|
932
1038
|
hooks: HOOKS,
|
|
933
1039
|
primitives: PRIMITIVES,
|
|
934
1040
|
manifestSchema: MANIFEST_SCHEMA,
|
|
935
1041
|
manifestCategories: CATEGORIES,
|
|
936
1042
|
manifestPlatforms: PLATFORMS,
|
|
1043
|
+
actionTriggerTypes: ACTION_TRIGGER_TYPES,
|
|
1044
|
+
actionScriptGlobals: ACTION_SCRIPT_GLOBALS,
|
|
1045
|
+
actionScriptMaxBytes: ACTION_SCRIPT_MAX_BYTES,
|
|
937
1046
|
themeTokens: DEFAULT_THEME_TOKENS,
|
|
938
1047
|
widgetContextShape: WIDGET_CONTEXT_SHAPE,
|
|
939
1048
|
bundleExportContract: BUNDLE_EXPORT_CONTRACT,
|
|
@@ -957,4 +1066,10 @@ function requiredContextKeys() {
|
|
|
957
1066
|
return [...keys];
|
|
958
1067
|
}
|
|
959
1068
|
|
|
960
|
-
module.exports = {
|
|
1069
|
+
module.exports = {
|
|
1070
|
+
CONTRACT,
|
|
1071
|
+
isHookAllowed,
|
|
1072
|
+
requiredContextKeys,
|
|
1073
|
+
widgetTranslationPrefix,
|
|
1074
|
+
widgetTranslationKey,
|
|
1075
|
+
};
|