@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/dist/index.cjs
CHANGED
|
@@ -26,6 +26,7 @@ __export(src_exports, {
|
|
|
26
26
|
CONTEXT_POLICIES: () => CONTEXT_POLICIES,
|
|
27
27
|
DELIVERIES: () => DELIVERIES,
|
|
28
28
|
ENTITY_TOKENS: () => ENTITY_TOKENS,
|
|
29
|
+
FIELD_KINDS: () => FIELD_KINDS,
|
|
29
30
|
FIELD_SENSITIVITIES: () => FIELD_SENSITIVITIES,
|
|
30
31
|
FIELD_SOURCES: () => FIELD_SOURCES,
|
|
31
32
|
FRESHNESS: () => FRESHNESS,
|
|
@@ -63,7 +64,11 @@ __export(src_exports, {
|
|
|
63
64
|
choiceSlug: () => choiceSlug,
|
|
64
65
|
choiceValuesOf: () => choiceValuesOf,
|
|
65
66
|
choicesOf: () => choicesOf,
|
|
67
|
+
coerceTypedAnswer: () => coerceTypedAnswer,
|
|
68
|
+
coerceTypedAnswers: () => coerceTypedAnswers,
|
|
69
|
+
coercionLabel: () => coercionLabel,
|
|
66
70
|
err: () => err,
|
|
71
|
+
fieldKindFor: () => fieldKindFor,
|
|
67
72
|
forgetAllOps: () => forgetAllOps,
|
|
68
73
|
highestLevel: () => highestLevel,
|
|
69
74
|
holdsARetiredChoice: () => holdsARetiredChoice,
|
|
@@ -215,6 +220,209 @@ function looksLikeAnId(value) {
|
|
|
215
220
|
return typeof value === "string" && UUID.test(value);
|
|
216
221
|
}
|
|
217
222
|
|
|
223
|
+
// src/coerce.ts
|
|
224
|
+
var FIELD_KINDS = [
|
|
225
|
+
"attachment",
|
|
226
|
+
"checkbox",
|
|
227
|
+
"currency",
|
|
228
|
+
"datetime",
|
|
229
|
+
"email",
|
|
230
|
+
"formula",
|
|
231
|
+
"json",
|
|
232
|
+
"long_text",
|
|
233
|
+
"lookup",
|
|
234
|
+
"member",
|
|
235
|
+
"multi_select",
|
|
236
|
+
"number",
|
|
237
|
+
"percent",
|
|
238
|
+
"phone",
|
|
239
|
+
"relation",
|
|
240
|
+
"rollup",
|
|
241
|
+
"select",
|
|
242
|
+
"text",
|
|
243
|
+
"url"
|
|
244
|
+
];
|
|
245
|
+
function fieldKindFor(field, kernel) {
|
|
246
|
+
const format = (field.format ?? "").toLowerCase();
|
|
247
|
+
const config = field.config ?? {};
|
|
248
|
+
switch (field.type) {
|
|
249
|
+
case "boolean":
|
|
250
|
+
return "checkbox";
|
|
251
|
+
case "text":
|
|
252
|
+
if (format === "email") return "email";
|
|
253
|
+
if (format === "phone") return "phone";
|
|
254
|
+
if (format === "url") return "url";
|
|
255
|
+
if (format === "json" || config["json"] === true) return "json";
|
|
256
|
+
if (format === "long" || config["multiline"] === true) return "long_text";
|
|
257
|
+
return "text";
|
|
258
|
+
case "range": {
|
|
259
|
+
const kind = String(config["kind"] ?? "number");
|
|
260
|
+
if (kind === "date" || kind === "datetime") return "datetime";
|
|
261
|
+
if (format === "currency") return "currency";
|
|
262
|
+
if (format === "percent" || field.unit === "%") return "percent";
|
|
263
|
+
return "number";
|
|
264
|
+
}
|
|
265
|
+
case "list":
|
|
266
|
+
return field.multi ? "multi_select" : "select";
|
|
267
|
+
case "relation": {
|
|
268
|
+
const parity = String(config["parity"] ?? "");
|
|
269
|
+
if (parity === "attachment" || parity === "member") return parity;
|
|
270
|
+
if (kernel?.file && field.relation_target === kernel.file) return "attachment";
|
|
271
|
+
if (kernel?.person && field.relation_target === kernel.person) return "member";
|
|
272
|
+
return "relation";
|
|
273
|
+
}
|
|
274
|
+
case "formula": {
|
|
275
|
+
const via = config["via"];
|
|
276
|
+
if (via && config["agg"]) return "rollup";
|
|
277
|
+
if (via) return "lookup";
|
|
278
|
+
return "formula";
|
|
279
|
+
}
|
|
280
|
+
default:
|
|
281
|
+
return "text";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
var TRUE_WORDS = /* @__PURE__ */ new Set(["true", "yes", "y", "1", "\u2713", "x", "checked", "on"]);
|
|
285
|
+
var FALSE_WORDS = /* @__PURE__ */ new Set(["false", "no", "n", "0", "", "unchecked", "off"]);
|
|
286
|
+
function coercionLabel(field) {
|
|
287
|
+
const label = (field.label ?? "").trim();
|
|
288
|
+
if (label !== "") return label;
|
|
289
|
+
const name = (field.name ?? "").trim();
|
|
290
|
+
if (name !== "") return name;
|
|
291
|
+
return field.key.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
|
|
292
|
+
}
|
|
293
|
+
function coerceTypedAnswer(field, raw, opts = {}) {
|
|
294
|
+
const label = opts.label ?? coercionLabel(field);
|
|
295
|
+
const text = raw.trim();
|
|
296
|
+
const kind = fieldKindFor(field, opts.kernel);
|
|
297
|
+
if (text === "" && kind !== "checkbox") return { value: null };
|
|
298
|
+
switch (kind) {
|
|
299
|
+
case "checkbox": {
|
|
300
|
+
const word = text.toLowerCase();
|
|
301
|
+
if (TRUE_WORDS.has(word)) return { value: true };
|
|
302
|
+
if (FALSE_WORDS.has(word)) return { value: word === "" ? null : false };
|
|
303
|
+
return {
|
|
304
|
+
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.`
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
case "number":
|
|
308
|
+
case "currency":
|
|
309
|
+
case "percent": {
|
|
310
|
+
const negative = /^\(.*\)$/.test(text);
|
|
311
|
+
const bare = text.replace(/^\(|\)$/g, "").replace(/[\s,]/g, "").replace(/^[^\d.\-+]+/, "").replace(/[^\d.\-+eE]+$/, "");
|
|
312
|
+
const n = Number(bare);
|
|
313
|
+
if (bare === "" || Number.isNaN(n)) {
|
|
314
|
+
return {
|
|
315
|
+
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.`
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return { value: negative ? -n : n };
|
|
319
|
+
}
|
|
320
|
+
case "datetime": {
|
|
321
|
+
const wantsTime = String(field.config?.["kind"] ?? "date") === "datetime";
|
|
322
|
+
const plainDay = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(raw);
|
|
323
|
+
if (!wantsTime && plainDay) return { value: `${plainDay[1]}-${plainDay[2]}-${plainDay[3]}` };
|
|
324
|
+
const at = Date.parse(text);
|
|
325
|
+
if (Number.isNaN(at)) {
|
|
326
|
+
return {
|
|
327
|
+
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.`
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const parsed = new Date(at);
|
|
331
|
+
if (wantsTime) return { value: parsed.toISOString() };
|
|
332
|
+
const y = parsed.getFullYear();
|
|
333
|
+
const m = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
334
|
+
const d = String(parsed.getDate()).padStart(2, "0");
|
|
335
|
+
return { value: `${y}-${m}-${d}` };
|
|
336
|
+
}
|
|
337
|
+
case "json": {
|
|
338
|
+
try {
|
|
339
|
+
return { value: JSON.parse(text) };
|
|
340
|
+
} catch {
|
|
341
|
+
return {
|
|
342
|
+
refusal: `${label} holds a document, and \u201C${raw}\u201D is not valid JSON. Write the whole object, braces and all.`
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
case "select":
|
|
347
|
+
case "multi_select": {
|
|
348
|
+
if (!opts.options) {
|
|
349
|
+
return {
|
|
350
|
+
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.`
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const wanted = kind === "multi_select" ? text.split(/[,;]/).map((w) => w.trim()).filter((w) => w.length > 0) : [text];
|
|
354
|
+
if (wanted.length === 0) return { value: kind === "multi_select" ? [] : null };
|
|
355
|
+
const matched = [];
|
|
356
|
+
const unmatched = [];
|
|
357
|
+
for (const word of wanted) {
|
|
358
|
+
const hit = opts.options.byWord.get(word.toLowerCase());
|
|
359
|
+
if (hit === void 0) unmatched.push(word);
|
|
360
|
+
else matched.push(hit);
|
|
361
|
+
}
|
|
362
|
+
if (unmatched.length > 0) {
|
|
363
|
+
const offered = opts.options.labels.length > 0 ? opts.options.labels.join(", ") : "it has none yet";
|
|
364
|
+
return {
|
|
365
|
+
refusal: `${label} does not have ${unmatched.map((w) => `\u201C${w}\u201D`).join(" or ")} among its options. It offers: ${offered}.`,
|
|
366
|
+
unmatched
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return { value: kind === "multi_select" ? matched : matched[0] };
|
|
370
|
+
}
|
|
371
|
+
// AN IDENTITY IS NOT A WORD. Writing the word would put a string where an
|
|
372
|
+
// id belongs, and every filter, colour and rollup built on that column
|
|
373
|
+
// would stop agreeing with it. So it is refused and the person is told
|
|
374
|
+
// which control does hold it.
|
|
375
|
+
case "member":
|
|
376
|
+
return {
|
|
377
|
+
refusal: `${label} names a person in this organization, so it is chosen rather than typed. Open it and pick them.`
|
|
378
|
+
};
|
|
379
|
+
case "attachment":
|
|
380
|
+
return {
|
|
381
|
+
refusal: `${label} holds a file, which cannot arrive as typed-in text. Open it and attach the file.`
|
|
382
|
+
};
|
|
383
|
+
case "relation":
|
|
384
|
+
return {
|
|
385
|
+
refusal: `${label} points at another record, so it is chosen rather than typed. Open it and pick the record.`
|
|
386
|
+
};
|
|
387
|
+
case "formula":
|
|
388
|
+
case "lookup":
|
|
389
|
+
case "rollup":
|
|
390
|
+
return {
|
|
391
|
+
refusal: `${label} is worked out by the system, so nothing can be written into it.`
|
|
392
|
+
};
|
|
393
|
+
default:
|
|
394
|
+
return { value: raw };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function coerceTypedAnswers(fields, answers, opts = {}) {
|
|
398
|
+
const byKey = new Map(fields.map((f) => [f.key, f]));
|
|
399
|
+
const values = {};
|
|
400
|
+
const refusals = {};
|
|
401
|
+
const unknownKeys = [];
|
|
402
|
+
for (const [key, given] of Object.entries(answers)) {
|
|
403
|
+
const field = byKey.get(key);
|
|
404
|
+
if (!field) {
|
|
405
|
+
unknownKeys.push(key);
|
|
406
|
+
values[key] = given;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (typeof given !== "string") {
|
|
410
|
+
values[key] = given;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const opt = {};
|
|
414
|
+
const choices = opts.options?.get(key);
|
|
415
|
+
if (choices) opt.options = choices;
|
|
416
|
+
if (opts.kernel) opt.kernel = opts.kernel;
|
|
417
|
+
const label = opts.labels?.[key];
|
|
418
|
+
if (label) opt.label = label;
|
|
419
|
+
const answered = coerceTypedAnswer(field, given, opt);
|
|
420
|
+
if ("refusal" in answered) refusals[key] = answered.refusal;
|
|
421
|
+
else values[key] = answered.value;
|
|
422
|
+
}
|
|
423
|
+
return { values, refusals, unknownKeys };
|
|
424
|
+
}
|
|
425
|
+
|
|
218
426
|
// src/pipeline.ts
|
|
219
427
|
var PIPELINE_RULE_KINDS = ["moves", "requires", "who", "limit", "entry"];
|
|
220
428
|
var STAGE_RULE_ON_FAIL = ["refuse", "require_approval"];
|
|
@@ -944,6 +1152,9 @@ var STORE_DOORS = [
|
|
|
944
1152
|
{ 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" },
|
|
945
1153
|
{ name: "per_value_access_words", args: "", returns: "text[]", security: "invoker" },
|
|
946
1154
|
{ name: "person_kernel_id", args: "", returns: "uuid", security: "invoker" },
|
|
1155
|
+
{ name: "phone_canonical", args: "p_text text", returns: "text", security: "invoker" },
|
|
1156
|
+
{ name: "phone_display", args: "p_text text", returns: "text", security: "invoker" },
|
|
1157
|
+
{ name: "phone_pattern", args: "", returns: "text", security: "invoker" },
|
|
947
1158
|
{ name: "pin_agent_cells", args: "p_new jsonb, p_old jsonb, p_agent_keys text[]", returns: "jsonb", security: "invoker" },
|
|
948
1159
|
{ 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" },
|
|
949
1160
|
{ name: "pipeline_declare", args: "p_organization_id uuid, p_table_id uuid, p_spec jsonb", returns: "jsonb", security: "definer" },
|
|
@@ -965,6 +1176,7 @@ var STORE_DOORS = [
|
|
|
965
1176
|
{ name: "portal_record_title", args: "p_organization_id uuid, p_record_id uuid", returns: "text", security: "definer" },
|
|
966
1177
|
{ name: "portal_revoke", args: "p_organization_id uuid, p_portal_id uuid, p_principal_id uuid", returns: "jsonb", security: "definer" },
|
|
967
1178
|
{ name: "portal_slug", args: "p_organization_id uuid, p_title text, p_portal_id uuid DEFAULT NULL::uuid", returns: "text", security: "definer" },
|
|
1179
|
+
{ name: "portal_tables", args: "p_organization_id uuid", returns: "TABLE(portal_id uuid, table_id uuid, name text)", security: "definer" },
|
|
968
1180
|
{ 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" },
|
|
969
1181
|
{ name: "presentation_kernel_id", args: "", returns: "uuid", security: "invoker" },
|
|
970
1182
|
{ name: "promote_field", args: "p_organization_id uuid, p_table_id uuid, p_field_id uuid", returns: "jsonb", security: "invoker" },
|
|
@@ -1243,7 +1455,7 @@ var STORE_REFUSAL_CODES = [
|
|
|
1243
1455
|
{ 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" },
|
|
1244
1456
|
{ code: "22007", raised_by: "dashboard_window_sql" },
|
|
1245
1457
|
{ code: "22012", raised_by: "rule_eval" },
|
|
1246
|
-
{ 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" },
|
|
1458
|
+
{ 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" },
|
|
1247
1459
|
{ 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" },
|
|
1248
1460
|
{ 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" },
|
|
1249
1461
|
{ 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" },
|
|
@@ -2143,6 +2355,7 @@ var ENTITY_TOKENS = [
|
|
|
2143
2355
|
"template_context_items",
|
|
2144
2356
|
"template_scope_types",
|
|
2145
2357
|
"templates",
|
|
2358
|
+
"test_handset_inbox",
|
|
2146
2359
|
"thread",
|
|
2147
2360
|
"tool",
|
|
2148
2361
|
"tool_binding",
|