@ai-matrx/records 0.44.1 → 0.46.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/CHANGELOG.md +49 -0
- package/dist/core/index.cjs +225 -1
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +14 -5
- package/dist/core/index.d.ts +14 -5
- package/dist/core/index.js +225 -1
- package/dist/core/index.js.map +1 -1
- package/dist/{field-D0Rr4Cqb.d.cts → field-N0ExInZA.d.cts} +22 -2
- package/dist/{field-D0Rr4Cqb.d.ts → field-N0ExInZA.d.ts} +22 -2
- package/dist/index.cjs +214 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -3
- package/dist/index.d.ts +146 -3
- package/dist/index.js +214 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +225 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +174 -3
- package/dist/react/index.d.ts +174 -3
- package/dist/react/index.js +225 -1
- package/dist/react/index.js.map +1 -1
- package/dist/use-cases/index.cjs.map +1 -1
- package/dist/use-cases/index.d.cts +1 -1
- package/dist/use-cases/index.d.ts +1 -1
- package/dist/use-cases/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/records
|
|
2
2
|
|
|
3
|
+
## 0.46.0
|
|
4
|
+
|
|
5
|
+
**A portal can say which Tables it shows.**
|
|
6
|
+
|
|
7
|
+
- **`portalTables()`** on the client and **`PortalTableRef`** — the one question a TABLE-scoped
|
|
8
|
+
Portals rail has to ask. `custom.portals` takes no table, so the rail on a Table's page (beside
|
|
9
|
+
Forms, Bookings and Dashboards, all table-scoped) decided emptiness from the ORGANIZATION's
|
|
10
|
+
portal count: on Rincon Plumbing Co's Parts table it read "Portals 10" and listed ten cards
|
|
11
|
+
belonging to Jobs, the empty state never rendered, and the whole "Build it / Ask an agent"
|
|
12
|
+
offer was unreachable on every table of that organization.
|
|
13
|
+
Same wall and same visibility gate as `portals`, so the two lists cannot disagree.
|
|
14
|
+
- Store registry regenerated against the live store: 559 doors, 1024 entity tokens.
|
|
15
|
+
|
|
16
|
+
## 0.45.0
|
|
17
|
+
|
|
18
|
+
**One coercion primitive: what a typed-in answer means to a Field, in one place.**
|
|
19
|
+
|
|
20
|
+
AGENT-BUILDS-2's closing walk, 2026-09-21, found Ironclad Mobile Mechanic's public booking
|
|
21
|
+
page **unbookable over any table with a number field**. A customer picked 2:00 PM, typed a
|
|
22
|
+
2019 Honda CR-V, pressed "Book it" and read:
|
|
23
|
+
|
|
24
|
+
> Vehicle Year takes a number, and it was given a string
|
|
25
|
+
|
|
26
|
+
and got no appointment. A browser input hands over a STRING; a Field takes a value. The
|
|
27
|
+
grid's paste and the CSV import each carried their own coercion — the two doors a STRANGER
|
|
28
|
+
uses carried none.
|
|
29
|
+
|
|
30
|
+
New, headless, and now the only body that answers the question:
|
|
31
|
+
|
|
32
|
+
- **`fieldKindFor(field, kernel)`** — the ONE classifier (behaviour, modifiers, format, unit,
|
|
33
|
+
relation target, config). `@ai-matrx/records-ui`'s `editorKindFor` is this function with
|
|
34
|
+
the kernel File/Person ids bound, so the editor a person types into and the coercion
|
|
35
|
+
applied to what they typed cannot disagree about a column.
|
|
36
|
+
- **`coerceTypedAnswer(field, raw, opts)`** — one string → the Field's value, or a refusal
|
|
37
|
+
naming the field **in plain words**, never its key. It coerces and never guesses: `0` for
|
|
38
|
+
"about twelve" is how a table quietly becomes untrue.
|
|
39
|
+
- **`coerceTypedAnswers(fields, answers, opts)`** — a whole screen at once. Values already
|
|
40
|
+
of the right shape are left alone; keys the table has no Field for are REPORTED rather
|
|
41
|
+
than dropped, because naming an unknown key is the store's sentence to say.
|
|
42
|
+
- **`CoercibleField`** — the structural shape the primitive needs, so `CaptureField` and the
|
|
43
|
+
Field documents `custom.form_public` / `custom.booking_public` publish to a stranger all
|
|
44
|
+
satisfy it. Demanding a whole `Field` is how the capture sheet kept its own `Number(raw)`.
|
|
45
|
+
- **`FIELD_KINDS`** — the runtime list `FieldKind` is derived from, so a coverage test walks
|
|
46
|
+
the source of truth rather than a retyped copy.
|
|
47
|
+
- **`ChoiceOptions`** moved here from records-ui, which re-exports it.
|
|
48
|
+
|
|
49
|
+
*Consumer action:* none required — everything is additive. A host that coerces typed-in text
|
|
50
|
+
itself should delete that code and call `coerceTypedAnswer`.
|
|
51
|
+
|
|
3
52
|
## 0.43.0
|
|
4
53
|
|
|
5
54
|
**The realtime port stops being half a contract, and a nudge re-reads only what moved.**
|
package/dist/core/index.cjs
CHANGED
|
@@ -40,6 +40,7 @@ __export(core_exports, {
|
|
|
40
40
|
DELIVERIES: () => DELIVERIES,
|
|
41
41
|
DOORS: () => DOORS,
|
|
42
42
|
ENTITY_TOKENS: () => ENTITY_TOKENS,
|
|
43
|
+
FIELD_KINDS: () => FIELD_KINDS,
|
|
43
44
|
FIELD_SENSITIVITIES: () => FIELD_SENSITIVITIES,
|
|
44
45
|
FIELD_SOURCES: () => FIELD_SOURCES,
|
|
45
46
|
FRESHNESS: () => FRESHNESS,
|
|
@@ -80,6 +81,9 @@ __export(core_exports, {
|
|
|
80
81
|
choiceSlug: () => choiceSlug,
|
|
81
82
|
choiceValuesOf: () => choiceValuesOf,
|
|
82
83
|
choicesOf: () => choicesOf,
|
|
84
|
+
coerceTypedAnswer: () => coerceTypedAnswer,
|
|
85
|
+
coerceTypedAnswers: () => coerceTypedAnswers,
|
|
86
|
+
coercionLabel: () => coercionLabel,
|
|
83
87
|
createRecordsClient: () => createRecordsClient,
|
|
84
88
|
doorAbsent: () => doorAbsent,
|
|
85
89
|
doorCensus: () => doorCensus,
|
|
@@ -87,6 +91,7 @@ __export(core_exports, {
|
|
|
87
91
|
err: () => err,
|
|
88
92
|
exportCsv: () => exportCsv,
|
|
89
93
|
exportXlsx: () => exportXlsx,
|
|
94
|
+
fieldKindFor: () => fieldKindFor,
|
|
90
95
|
forgetAllOps: () => forgetAllOps,
|
|
91
96
|
highestLevel: () => highestLevel,
|
|
92
97
|
holdsARetiredChoice: () => holdsARetiredChoice,
|
|
@@ -251,6 +256,209 @@ function looksLikeAnId(value) {
|
|
|
251
256
|
return typeof value === "string" && UUID.test(value);
|
|
252
257
|
}
|
|
253
258
|
|
|
259
|
+
// src/coerce.ts
|
|
260
|
+
var FIELD_KINDS = [
|
|
261
|
+
"attachment",
|
|
262
|
+
"checkbox",
|
|
263
|
+
"currency",
|
|
264
|
+
"datetime",
|
|
265
|
+
"email",
|
|
266
|
+
"formula",
|
|
267
|
+
"json",
|
|
268
|
+
"long_text",
|
|
269
|
+
"lookup",
|
|
270
|
+
"member",
|
|
271
|
+
"multi_select",
|
|
272
|
+
"number",
|
|
273
|
+
"percent",
|
|
274
|
+
"phone",
|
|
275
|
+
"relation",
|
|
276
|
+
"rollup",
|
|
277
|
+
"select",
|
|
278
|
+
"text",
|
|
279
|
+
"url"
|
|
280
|
+
];
|
|
281
|
+
function fieldKindFor(field, kernel) {
|
|
282
|
+
const format = (field.format ?? "").toLowerCase();
|
|
283
|
+
const config = field.config ?? {};
|
|
284
|
+
switch (field.type) {
|
|
285
|
+
case "boolean":
|
|
286
|
+
return "checkbox";
|
|
287
|
+
case "text":
|
|
288
|
+
if (format === "email") return "email";
|
|
289
|
+
if (format === "phone") return "phone";
|
|
290
|
+
if (format === "url") return "url";
|
|
291
|
+
if (format === "json" || config["json"] === true) return "json";
|
|
292
|
+
if (format === "long" || config["multiline"] === true) return "long_text";
|
|
293
|
+
return "text";
|
|
294
|
+
case "range": {
|
|
295
|
+
const kind = String(config["kind"] ?? "number");
|
|
296
|
+
if (kind === "date" || kind === "datetime") return "datetime";
|
|
297
|
+
if (format === "currency") return "currency";
|
|
298
|
+
if (format === "percent" || field.unit === "%") return "percent";
|
|
299
|
+
return "number";
|
|
300
|
+
}
|
|
301
|
+
case "list":
|
|
302
|
+
return field.multi ? "multi_select" : "select";
|
|
303
|
+
case "relation": {
|
|
304
|
+
const parity = String(config["parity"] ?? "");
|
|
305
|
+
if (parity === "attachment" || parity === "member") return parity;
|
|
306
|
+
if (kernel?.file && field.relation_target === kernel.file) return "attachment";
|
|
307
|
+
if (kernel?.person && field.relation_target === kernel.person) return "member";
|
|
308
|
+
return "relation";
|
|
309
|
+
}
|
|
310
|
+
case "formula": {
|
|
311
|
+
const via = config["via"];
|
|
312
|
+
if (via && config["agg"]) return "rollup";
|
|
313
|
+
if (via) return "lookup";
|
|
314
|
+
return "formula";
|
|
315
|
+
}
|
|
316
|
+
default:
|
|
317
|
+
return "text";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
|
|
321
|
+
var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
|
|
322
|
+
function coercionLabel(field) {
|
|
323
|
+
const label = (field.label ?? "").trim();
|
|
324
|
+
if (label !== "") return label;
|
|
325
|
+
const name = (field.name ?? "").trim();
|
|
326
|
+
if (name !== "") return name;
|
|
327
|
+
return field.key.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
|
|
328
|
+
}
|
|
329
|
+
function coerceTypedAnswer(field, raw, opts = {}) {
|
|
330
|
+
const label = opts.label ?? coercionLabel(field);
|
|
331
|
+
const text = raw.trim();
|
|
332
|
+
const kind = fieldKindFor(field, opts.kernel);
|
|
333
|
+
if (text === "" && kind !== "checkbox") return { value: null };
|
|
334
|
+
switch (kind) {
|
|
335
|
+
case "checkbox": {
|
|
336
|
+
const word = text.toLowerCase();
|
|
337
|
+
if (TRUE_WORDS.has(word)) return { value: true };
|
|
338
|
+
if (FALSE_WORDS.has(word)) return { value: word === "" ? null : false };
|
|
339
|
+
return {
|
|
340
|
+
refusal: `${label} is a tick box, so it takes yes or no \u2014 \u201C${raw}\u201D is neither. Write TRUE/FALSE, yes/no or 1/0.`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
case "number":
|
|
344
|
+
case "currency":
|
|
345
|
+
case "percent": {
|
|
346
|
+
const negative = /^\(.*\)$/.test(text);
|
|
347
|
+
const bare = text.replace(/^\(|\)$/g, "").replace(/[\s,]/g, "").replace(/^[^\d.\-+]+/, "").replace(/[^\d.\-+eE]+$/, "");
|
|
348
|
+
const n = Number(bare);
|
|
349
|
+
if (bare === "" || Number.isNaN(n)) {
|
|
350
|
+
return {
|
|
351
|
+
refusal: `${label} holds a number, and \u201C${raw}\u201D is not one. Write digits \u2014 a currency symbol, a percent sign, commas and brackets for a negative are all fine.`
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return { value: negative ? -n : n };
|
|
355
|
+
}
|
|
356
|
+
case "datetime": {
|
|
357
|
+
const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
|
|
358
|
+
const plainDay = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(raw);
|
|
359
|
+
if (!wantsTime && plainDay) return { value: `${plainDay[1]}-${plainDay[2]}-${plainDay[3]}` };
|
|
360
|
+
const at = Date.parse(text);
|
|
361
|
+
if (Number.isNaN(at)) {
|
|
362
|
+
return {
|
|
363
|
+
refusal: `${label} holds a date, and \u201C${raw}\u201D is not one this can read. Write it as 2026-09-21, or the way your spreadsheet writes dates.`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const parsed = new Date(at);
|
|
367
|
+
if (wantsTime) return { value: parsed.toISOString() };
|
|
368
|
+
const y = parsed.getFullYear();
|
|
369
|
+
const m = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
370
|
+
const d = String(parsed.getDate()).padStart(2, "0");
|
|
371
|
+
return { value: `${y}-${m}-${d}` };
|
|
372
|
+
}
|
|
373
|
+
case "json": {
|
|
374
|
+
try {
|
|
375
|
+
return { value: JSON.parse(text) };
|
|
376
|
+
} catch {
|
|
377
|
+
return {
|
|
378
|
+
refusal: `${label} holds a document, and \u201C${raw}\u201D is not valid JSON. Write the whole object, braces and all.`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
case "select":
|
|
383
|
+
case "multi_select": {
|
|
384
|
+
if (!opts.options) {
|
|
385
|
+
return {
|
|
386
|
+
refusal: `${label} is a list and its options have not been read yet, so nothing can be matched to \u201C${raw}\u201D. Try again once the column's choices have loaded.`
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const wanted = kind === "multi_select" ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text];
|
|
390
|
+
if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
|
|
391
|
+
const matched = [];
|
|
392
|
+
const unmatched = [];
|
|
393
|
+
for (const word of wanted) {
|
|
394
|
+
const hit = opts.options.byWord.get(word.toLowerCase());
|
|
395
|
+
if (hit === void 0) unmatched.push(word);
|
|
396
|
+
else matched.push(hit);
|
|
397
|
+
}
|
|
398
|
+
if (unmatched.length > 0) {
|
|
399
|
+
const offered = opts.options.labels.length > 0 ? opts.options.labels.join(", ") : "it has none yet";
|
|
400
|
+
return {
|
|
401
|
+
refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
|
|
402
|
+
unmatched
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
return { value: kind === "multi_select" ? matched : matched[0] };
|
|
406
|
+
}
|
|
407
|
+
// AN IDENTITY IS NOT A WORD. Writing the word would put a string where an
|
|
408
|
+
// id belongs, and every filter, colour and rollup built on that column
|
|
409
|
+
// would stop agreeing with it. So it is refused and the person is told
|
|
410
|
+
// which control does hold it.
|
|
411
|
+
case "member":
|
|
412
|
+
return {
|
|
413
|
+
refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open it and pick them.`
|
|
414
|
+
};
|
|
415
|
+
case "attachment":
|
|
416
|
+
return {
|
|
417
|
+
refusal: `${label} holds a file, which cannot arrive as typed-in text. Open it and attach the file.`
|
|
418
|
+
};
|
|
419
|
+
case "relation":
|
|
420
|
+
return {
|
|
421
|
+
refusal: `${label} points at another record, so it is chosen rather than typed. Open it and pick the record.`
|
|
422
|
+
};
|
|
423
|
+
case "formula":
|
|
424
|
+
case "lookup":
|
|
425
|
+
case "rollup":
|
|
426
|
+
return {
|
|
427
|
+
refusal: `${label} is worked out by the system, so nothing can be written into it.`
|
|
428
|
+
};
|
|
429
|
+
default:
|
|
430
|
+
return { value: raw };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function coerceTypedAnswers(fields, answers, opts = {}) {
|
|
434
|
+
const byKey = new Map(fields.map((f) => [f.key, f]));
|
|
435
|
+
const values = {};
|
|
436
|
+
const refusals = {};
|
|
437
|
+
const unknownKeys = [];
|
|
438
|
+
for (const [key, given] of Object.entries(answers)) {
|
|
439
|
+
const field = byKey.get(key);
|
|
440
|
+
if (!field) {
|
|
441
|
+
unknownKeys.push(key);
|
|
442
|
+
values[key] = given;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (typeof given !== "string") {
|
|
446
|
+
values[key] = given;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const opt = {};
|
|
450
|
+
const choices = opts.options?.get(key);
|
|
451
|
+
if (choices) opt.options = choices;
|
|
452
|
+
if (opts.kernel) opt.kernel = opts.kernel;
|
|
453
|
+
const label = opts.labels?.[key];
|
|
454
|
+
if (label) opt.label = label;
|
|
455
|
+
const answered = coerceTypedAnswer(field, given, opt);
|
|
456
|
+
if ("refusal" in answered) refusals[key] = answered.refusal;
|
|
457
|
+
else values[key] = answered.value;
|
|
458
|
+
}
|
|
459
|
+
return { values, refusals, unknownKeys };
|
|
460
|
+
}
|
|
461
|
+
|
|
254
462
|
// src/pipeline.ts
|
|
255
463
|
var PIPELINE_RULE_KINDS = ["moves", "requires", "who", "limit", "entry"];
|
|
256
464
|
var STAGE_RULE_ON_FAIL = ["refuse", "require_approval"];
|
|
@@ -980,6 +1188,9 @@ var STORE_DOORS = [
|
|
|
980
1188
|
{ name: "parity_values", args: "p_organization_id uuid, p_record_id uuid", returns: "TABLE(field_key text, field_id uuid, parity_type text, behavior text, unit text, format text, value jsonb, value_version integer, actor text, written_at timestamp with time zone)", security: "invoker" },
|
|
981
1189
|
{ name: "per_value_access_words", args: "", returns: "text[]", security: "invoker" },
|
|
982
1190
|
{ name: "person_kernel_id", args: "", returns: "uuid", security: "invoker" },
|
|
1191
|
+
{ name: "phone_canonical", args: "p_text text", returns: "text", security: "invoker" },
|
|
1192
|
+
{ name: "phone_display", args: "p_text text", returns: "text", security: "invoker" },
|
|
1193
|
+
{ name: "phone_pattern", args: "", returns: "text", security: "invoker" },
|
|
983
1194
|
{ name: "pin_agent_cells", args: "p_new jsonb, p_old jsonb, p_agent_keys text[]", returns: "jsonb", security: "invoker" },
|
|
984
1195
|
{ name: "pipeline_board", args: "p_organization_id uuid, p_table_id uuid, p_measure text DEFAULT NULL::text", returns: "TABLE(stage_key text, stage_label text, stage_position integer, cards bigint, total numeric, wip_limit integer, over_limit boolean)", security: "definer" },
|
|
985
1196
|
{ name: "pipeline_declare", args: "p_organization_id uuid, p_table_id uuid, p_spec jsonb", returns: "jsonb", security: "definer" },
|
|
@@ -1001,6 +1212,7 @@ var STORE_DOORS = [
|
|
|
1001
1212
|
{ name: "portal_record_title", args: "p_organization_id uuid, p_record_id uuid", returns: "text", security: "definer" },
|
|
1002
1213
|
{ name: "portal_revoke", args: "p_organization_id uuid, p_portal_id uuid, p_principal_id uuid", returns: "jsonb", security: "definer" },
|
|
1003
1214
|
{ name: "portal_slug", args: "p_organization_id uuid, p_title text, p_portal_id uuid DEFAULT NULL::uuid", returns: "text", security: "definer" },
|
|
1215
|
+
{ name: "portal_tables", args: "p_organization_id uuid", returns: "TABLE(portal_id uuid, table_id uuid, name text)", security: "definer" },
|
|
1004
1216
|
{ name: "portals", args: "p_organization_id uuid", returns: "TABLE(portal_id uuid, title text, slug text, client_table_id uuid, client_table text, is_active boolean, tables integer, invited integer, signed_in integer, sign_in_method text, opened_at timestamp with time zone)", security: "definer" },
|
|
1005
1217
|
{ name: "presentation_kernel_id", args: "", returns: "uuid", security: "invoker" },
|
|
1006
1218
|
{ name: "promote_field", args: "p_organization_id uuid, p_table_id uuid, p_field_id uuid", returns: "jsonb", security: "invoker" },
|
|
@@ -1279,7 +1491,7 @@ var STORE_REFUSAL_CODES = [
|
|
|
1279
1491
|
{ code: "22004", raised_by: "_entity_custom_fields_guard,_value_envelope,actor_word,agg_assert_key,agg_sql,anon_capture,anon_token_issue,anon_write,assert_client_may_reach,booking_declare,capture_submit,checklist_declare,context_resolve,dashboard_block_normalize,dashboard_declare,dashboard_window_sql,doc_render_write,doc_sign,doc_signature_write,doc_template_save,enrich_land,enrich_normalize,external_history_event,external_rows,external_source_declare,external_stub_upsert,external_write_through,external_writes_set,field_history,form_declare,form_submit,home_add,io_comment_write,io_import_begin,io_infer_column,io_outbox_drain,io_restore,migrate_choice_keys,migrate_purge,migrate_reclass,migrate_split,migrate_undo,organization_clear,organization_contents,pipeline_declare,pipeline_gate_preview,portal_declare,portal_invite,portal_principal_bind,provenance_retention_days,provenance_write,query_across_homes,query_by_coordinates,query_rollup,record_delete,record_reparent,record_resolve,record_restore,record_update,record_write,record_write_many,relation_carry,relation_own,rule_declare,share_grant,sign_request_create,sign_request_sign,subscription_declare,table_declare,table_share_outside_invite,value_restore,view_declare,work_approval_decide,work_approval_request,work_template_declare" },
|
|
1280
1492
|
{ code: "22007", raised_by: "dashboard_window_sql" },
|
|
1281
1493
|
{ code: "22012", raised_by: "rule_eval" },
|
|
1282
|
-
{ code: "22023", raised_by: "_booking_availability,_entity_custom_fields_guard,_take_op_id,_value_envelope,_work_shape_guard,actor_word,agg_assert_key,agg_sql,anon_token_issue,booking_declare,containment_parent,dashboard_block_normalize,dashboard_window_sql,doc_render_body,doc_render_write,doc_signature_write,enrich_declare,enrich_land,enrich_normalize,enrich_pin,entity_value_write,external_history_event,external_source_declare,external_stub_upsert,external_write_through,field_history,io_export,io_import_begin,io_import_finish,io_import_plan,io_import_report,io_proposal_accept,migrate_merge,migrate_reclass,my_levels,organization_clear,page_size,pin_agent_cells,pipeline_move,portal_declare,provenance_write,query_by_coordinates,query_rollup,record_update,record_write_many,relation_carry,rule_eval,rule_members,share_grant,share_lane_set,sign_request_create,sign_request_sign,subscription_declare,table_rules,visibility_as_of,work_approval_request,work_list,work_slot_hold" },
|
|
1494
|
+
{ code: "22023", raised_by: "_booking_availability,_entity_custom_fields_guard,_take_op_id,_value_envelope,_work_shape_guard,actor_word,agg_assert_key,agg_sql,anon_token_issue,booking_declare,containment_parent,dashboard_block_normalize,dashboard_window_sql,doc_render_body,doc_render_write,doc_signature_write,enrich_declare,enrich_land,enrich_normalize,enrich_pin,entity_value_write,external_history_event,external_source_declare,external_stub_upsert,external_write_through,field_history,field_update,io_export,io_import_begin,io_import_finish,io_import_plan,io_import_report,io_proposal_accept,migrate_merge,migrate_reclass,my_levels,organization_clear,page_size,pin_agent_cells,pipeline_move,portal_declare,provenance_write,query_by_coordinates,query_rollup,record_update,record_write_many,relation_carry,rule_eval,rule_members,share_grant,share_lane_set,sign_request_create,sign_request_sign,subscription_declare,table_rules,visibility_as_of,work_approval_request,work_list,work_slot_hold" },
|
|
1283
1495
|
{ code: "23503", raised_by: "_booking_availability,_checklist_instantiate,_containment_guard,_display_spec_for,_field_document_for,_pipeline_stage_key,_record_rule_uses,agg_digest_assemble,anon_publish,anon_rate_take,anon_token_issue,anon_write,assert_organization_wall,booking_cancel,booking_confirm,booking_declare,booking_hold,booking_reschedule,capture_publish,checklist_declare,checklist_steps_table,comment_write,dashboard_declare,delete_rule,doc_sign,doc_template_save,enrich_declare,enrich_due,enrich_land,form_declare,form_submit,home_add,inbound_declare,inbound_mail_land,inbound_set,io_comment_write,io_import_begin,io_import_finish,io_import_report,io_import_rows,io_proposal_accept,migrate_demote,migrate_promote,pipeline_board,pipeline_declare,pipeline_move,pipeline_read,pipeline_transition_refusal,promote_field,promote_table,record_applicability,record_reparent,relation_carry,relation_own,resolve_first_match,rule_applies,rule_declare,rule_eval,rule_members,rule_run,sign_request_create,subscription_mute,subscription_preview,view_declare,work_set_state,work_take_assignment,work_template_instantiate" },
|
|
1284
1496
|
{ code: "23505", raised_by: "_unique_rule_holds,doc_sign,doc_signature_write,entity_field_declare,field_declare,home_add,inbound_declare,io_proposal_accept,relation_carry,relation_own,share_grant,sign_request_cancel,sign_request_create,table_share_outside_invite,table_share_outside_resend,work_approval_decide,work_slot_hold" },
|
|
1285
1497
|
{ code: "23514", raised_by: "_checklist_instantiate,_checklist_step_guard,_containment_guard,_dated_values_guard,_derived_fields,_display_spec_for,_entity_custom_fields_guard,_field_class_guard,_field_document_for,_field_shape_guard,_field_type_parity_guard,_merge_field_shape_guard,_merge_field_temporal_guard,_promoted_field_cap_guard,_record_rule_uses,_resolve_choice_words,_rule_shape_guard,_rule_topology_guard,_store_relation_edge_names_its_field,_table_columns_word_guard,_table_shape_guard,_undeclared_key_guard,_value_envelope,_with_display,_work_shape_guard,_workdoors_approval_guard,assert_columns_are_defined,assert_entity_is_organization_scoped,booking_declare,capture_publish,capture_sheet_declare,checklist_declare,checklist_step_complete,doc_sign,doc_signature_write,doc_template_save,entity_field_declare,entity_field_retire,entity_field_update,entity_records_find,entity_table,field_declare,field_retire,field_update,home_add,io_import_rows,migrate_merge,relation_carry,relation_own,sign_request_create,sign_request_sign,table_declare,validate_value_envelope,validate_values,work_set_state,work_slots_declare,work_take_assignment,work_template_instantiate" },
|
|
@@ -2179,6 +2391,7 @@ var ENTITY_TOKENS = [
|
|
|
2179
2391
|
"template_context_items",
|
|
2180
2392
|
"template_scope_types",
|
|
2181
2393
|
"templates",
|
|
2394
|
+
"test_handset_inbox",
|
|
2182
2395
|
"thread",
|
|
2183
2396
|
"tool",
|
|
2184
2397
|
"tool_binding",
|
|
@@ -2548,6 +2761,14 @@ var DOORS = {
|
|
|
2548
2761
|
// pair (`custom.portal_public`, `custom.portal_invitation`, `custom.portal_me`)
|
|
2549
2762
|
// is deliberately absent here: those belong to the outsider's own shell.
|
|
2550
2763
|
portals: "portals",
|
|
2764
|
+
// WHICH TABLES EACH PORTAL SHOWS (TAILS-3). The Portals RAIL lives on a
|
|
2765
|
+
// Table's page beside Forms, Bookings and Dashboards, all of which are
|
|
2766
|
+
// table-scoped; `custom.portals` takes no table, so an organization with one
|
|
2767
|
+
// portal over Jobs saw that portal's cards on its Parts table, the empty
|
|
2768
|
+
// state never rendered, and the whole build/ask offer was unreachable
|
|
2769
|
+
// everywhere in that organization. This is the one question the rail has to
|
|
2770
|
+
// ask, under the same wall and visibility gate as `portals` itself.
|
|
2771
|
+
portalTables: "portal_tables",
|
|
2551
2772
|
portalCard: "portal_card",
|
|
2552
2773
|
portalDeclare: "portal_declare",
|
|
2553
2774
|
portalInvite: "portal_invite",
|
|
@@ -3613,6 +3834,9 @@ function createRecordsClient(config) {
|
|
|
3613
3834
|
async portals() {
|
|
3614
3835
|
return callDoor(DOORS.portals, { p_organization_id: org }, "portals");
|
|
3615
3836
|
},
|
|
3837
|
+
async portalTables() {
|
|
3838
|
+
return callDoor(DOORS.portalTables, { p_organization_id: org }, "portalTables");
|
|
3839
|
+
},
|
|
3616
3840
|
async portalCard({ portal_id }) {
|
|
3617
3841
|
return callDoor(
|
|
3618
3842
|
DOORS.portalCard,
|