@salesforce/afv-skills 1.62.0 → 1.64.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/package.json +1 -1
- package/skills/agentforce-offline-experimentation-run/SKILL.md +253 -0
- package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +796 -0
- package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +111 -0
- package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +1053 -0
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +21 -11
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +10 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +33 -1
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +44 -1
- package/skills/automation-sandbox-post-copy-configure/SKILL.md +132 -124
- package/skills/automation-sandbox-post-copy-configure/references/flow_activation_path.md +151 -0
- package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +55 -9
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +54 -107
- package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +1 -1
- package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +82 -5
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +35 -2
- package/skills/consumer-goods-promotion-bo-api-deploy/references/preflight-and-seed-queries.md +149 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +17 -1
- package/skills/dx-devops-conflict-resolve/SKILL.md +1 -5
- package/skills/dx-devops-pipeline-manage/SKILL.md +1 -5
- package/skills/dx-devops-project-manage/SKILL.md +1 -5
- package/skills/dx-devops-promote/SKILL.md +1 -5
- package/skills/dx-devops-request-status/SKILL.md +1 -5
- package/skills/dx-lifecycle-center/SKILL.md +120 -0
- package/skills/dx-lifecycle-center/examples/workflows.md +175 -0
- package/skills/dx-lifecycle-center/references/connect-api.md +123 -0
- package/skills/dx-lifecycle-center/references/connect-org.md +184 -0
- package/skills/dx-lifecycle-center/references/deploy.md +153 -0
- package/skills/dx-lifecycle-center/references/error-handling.md +21 -0
- package/skills/dx-lifecycle-center/references/hub-org.md +37 -0
- package/skills/dx-lifecycle-center/references/list-orgs.md +99 -0
- package/skills/dx-lifecycle-center/references/request-body.md +75 -0
- package/skills/dx-lifecycle-center/references/troubleshooting.md +31 -0
- package/skills/dx-lifecycle-center/scripts/poll-deploy-status.sh +200 -0
- package/skills/dx-lifecycle-center/scripts/resolve-environment.sh +120 -0
- package/skills/dx-org-devhub-configure/SKILL.md +1 -3
- package/skills/dx-org-shape-manage/SKILL.md +1 -5
- package/skills/dx-org-snapshot-manage/SKILL.md +1 -3
- package/skills/education-cloud-academic-calendar-generate/SKILL.md +1 -3
- package/skills/education-cloud-course-catalog-migrate/SKILL.md +1 -3
- package/skills/education-cloud-domain-configure/SKILL.md +1 -3
- package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -3
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -5
- package/skills/experience-ui-bundle-2gp-deploy/SKILL.md +1 -3
- package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -5
- package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -3
- package/skills/field-service-data-capture-migrate/SKILL.md +1 -3
- package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +1 -3
- package/skills/marketing-message-test/SKILL.md +1 -3
- package/skills/nonprofit-fundraising-configure/SKILL.md +240 -0
- package/skills/nonprofit-fundraising-configure/references/gotchas.md +26 -0
- package/skills/nonprofit-fundraising-configure/references/mcp-invocation.md +678 -0
- package/skills/nonprofit-fundraising-configure/references/permission-targeting.md +35 -0
- package/skills/nonprofit-volunteer-management-model-get/SKILL.md +166 -0
- package/skills/nonprofit-volunteer-management-model-get/references/entities.md +315 -0
- package/skills/nonprofit-volunteer-management-model-get/references/relationships.md +175 -0
- package/skills/nonprofit-volunteer-management-model-get/references/themes.md +143 -0
- package/skills/nonprofit-volunteer-management-records-create/SKILL.md +121 -0
- package/skills/nonprofit-volunteer-management-records-create/references/field-guide.md +284 -0
- package/skills/nonprofit-volunteer-management-records-create/references/mcp-invocation.md +248 -0
- package/skills/nonprofit-volunteer-management-records-create/references/review-format.md +140 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/expand-series.py +632 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/render-review-table.py +419 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_expand_series.py +690 -0
- package/skills/nonprofit-volunteer-management-records-create/scripts/tests/test_render_review_table.py +421 -0
- package/skills/nonprofit-volunteer-management-structure-generate/SKILL.md +119 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/configuration-decisions.md +159 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/discovery-interview.md +137 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/edge-cases.md +48 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/mcp-invocation.md +75 -0
- package/skills/nonprofit-volunteer-management-structure-generate/references/plan-output.md +141 -0
- package/skills/platform-custom-field-generate/SKILL.md +1 -0
- package/skills/platform-custom-field-generate/references/advanced-picklists.md +5 -5
- package/skills/platform-custom-metadata-type-generate/SKILL.md +3 -0
- package/skills/platform-custom-object-generate/SKILL.md +4 -0
- package/skills/platform-custom-setting-generate/SKILL.md +3 -0
- package/skills/platform-datamask-run/SKILL.md +1 -5
- package/skills/platform-permission-set-generate/SKILL.md +3 -0
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +1 -3
- package/skills/platform-sandbox-configure/SKILL.md +1 -3
- package/skills/platform-value-set-generate/SKILL.md +3 -0
- package/skills/sales-call-scoring-configure/SKILL.md +1 -5
- package/skills/service-agentforce-channel-configure/SKILL.md +1 -3
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +1 -3
- package/skills/service-catalog-template-coordinate/SKILL.md +1 -3
- package/skills/service-concierge-portal-generate/SKILL.md +1 -3
- package/skills/service-email-to-case-configure/SKILL.md +1 -5
- package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-agentforce-studio-validate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -5
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +1 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +1 -0
- package/skills/service-itsm-agentic-setup-itsm-agentforce-permset-assign/SKILL.md +1 -3
- package/skills/service-itsm-agentic-setup-uel-user-create/SKILL.md +1 -9
- package/skills/service-itsm-incident-mgmt-configure/SKILL.md +1 -5
- package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -3
- package/skills/service-itsm-slack-itservice-configure/SKILL.md +1 -3
- package/skills/service-itsm-swarming-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-debug/SKILL.md +1 -3
- package/skills/service-itsm-teams-employee-agent-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-itdesk-configure/SKILL.md +1 -3
- package/skills/service-itsm-teams-itservice-configure/SKILL.md +1 -3
- package/skills/service-native-voice-recording-transcription-configure/SKILL.md +1 -3
- package/skills/service-omni-agent-users-create/SKILL.md +1 -3
- package/skills/service-omni-agent-work-sharing-configure/SKILL.md +1 -3
- package/skills/service-omni-attribute-routing-configure/SKILL.md +1 -3
- package/skills/service-omni-base-settings-configure/SKILL.md +1 -3
- package/skills/service-omni-channel-inventory-analyze/SKILL.md +1 -3
- package/skills/service-omni-channel-limits-analyze/SKILL.md +1 -3
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +1 -3
- package/skills/service-omni-command-center-analyze/SKILL.md +1 -3
- package/skills/service-omni-command-center-configure/SKILL.md +1 -3
- package/skills/service-omni-permission-set-assign/SKILL.md +1 -3
- package/skills/service-omni-presence-status-deploy/SKILL.md +1 -3
- package/skills/service-omni-presence-user-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-queue-deploy/SKILL.md +1 -3
- package/skills/service-omni-queue-members-assign/SKILL.md +1 -3
- package/skills/service-omni-queue-routing-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-routing-flow-deploy/SKILL.md +1 -3
- package/skills/service-omni-service-channel-configure/SKILL.md +1 -3
- package/skills/service-omni-sidebar-configure/SKILL.md +1 -3
- package/skills/service-omni-skills-based-routing-configure/SKILL.md +1 -3
- package/skills/service-omni-supervisor-config-deploy/SKILL.md +1 -3
- package/skills/service-omni-supervisor-permset-assign/SKILL.md +1 -3
- package/skills/service-omni-supervisor-surface-deploy/SKILL.md +1 -3
- package/skills/service-omni-supervisor-users-create/SKILL.md +1 -3
- package/skills/service-omni-work-skill-routing-configure/SKILL.md +1 -3
- package/skills/field-service-setup-orchestrator-get/SKILL.md +0 -47
- package/skills/tableau-next-asset-create/README.md +0 -65
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render one entity's "Records to insert" review table with a fixed column order.
|
|
3
|
+
|
|
4
|
+
The review table is the user's last look before a write, so a cell that lands under the
|
|
5
|
+
wrong header (a Status value showing under Description) is a real defect, not cosmetics.
|
|
6
|
+
That happens when rows are typed out by hand, cell by cell, and the fields of each record
|
|
7
|
+
arrive in a different order. This script removes the hand step: the agent passes the
|
|
8
|
+
entity's records as data (field -> value + origin tag) and the script prints the markdown
|
|
9
|
+
table. The column order is decided once, for every row:
|
|
10
|
+
|
|
11
|
+
1. ``Record`` — row number and the record's name;
|
|
12
|
+
2. the entity's non-lookup fields, in ``references/field-guide.md`` tier order
|
|
13
|
+
(T1, then T2, then T3; ties keep the field guide's row order), then any custom fields;
|
|
14
|
+
3. lookup fields last, in the same tier order.
|
|
15
|
+
|
|
16
|
+
Field facts (API names, tiers, which fields are lookups, which are schema-required) are read
|
|
17
|
+
from ``references/field-guide.md`` so the table and the field guide can't drift apart.
|
|
18
|
+
Headers are field labels ("Parent Volunteer Initiative"), never API names: a label passed in
|
|
19
|
+
``labels`` (from a live describe, so an admin rename wins) is used first; otherwise the label
|
|
20
|
+
is spelled out from the field guide's field name.
|
|
21
|
+
|
|
22
|
+
USAGE
|
|
23
|
+
-----
|
|
24
|
+
python3 scripts/render-review-table.py < records.json
|
|
25
|
+
python3 scripts/render-review-table.py --expand < records.json # list grouped rows one by one
|
|
26
|
+
|
|
27
|
+
SPEC (stdin JSON)
|
|
28
|
+
-----------------
|
|
29
|
+
{
|
|
30
|
+
"entity": "JobPosition",
|
|
31
|
+
"records": [
|
|
32
|
+
{"fields": {
|
|
33
|
+
"Name": {"value": "Greeter — Eastside", "origin": "given"},
|
|
34
|
+
"Title": {"value": "Greeter", "origin": "generated"},
|
|
35
|
+
"Status": {"value": "Upcoming", "origin": "default"},
|
|
36
|
+
"VolunteerInitiativeId": {"value": "Food Pantry 2027", "origin": "new"}
|
|
37
|
+
}}
|
|
38
|
+
],
|
|
39
|
+
"labels": {"VolunteerInitiativeId": "Related Volunteer Initiative"}, # optional
|
|
40
|
+
"requiredCustomFields": ["Region__c"], # optional
|
|
41
|
+
"title": "Records to insert — 12 Job Position Shifts (1 series)" # optional
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
* Fields are keyed by API name (``VolunteerInitiativeId``) or by the field guide's
|
|
45
|
+
relationship name (``VolunteerInitiative``); both resolve to the same column.
|
|
46
|
+
* ``origin`` is one of: given, default, generated, reuse, new, needs-input. A lookup cell
|
|
47
|
+
is tagged ``reuse`` (an existing record) or ``new`` (created this run) and shows the
|
|
48
|
+
referenced record's Name, never an Id. The origin is checked, but only ``reuse`` and
|
|
49
|
+
``needs-input`` are printed: every other value is reviewed the same way whatever its
|
|
50
|
+
origin.
|
|
51
|
+
* A record may carry ``"label"`` to name its row; otherwise the row uses its ``Name`` value.
|
|
52
|
+
* A required field (schema-required or always-set in the field guide) always gets a column.
|
|
53
|
+
When a record has no value for it the cell shows ``needs-input``. A field that is always
|
|
54
|
+
set but not schema-required (e.g. an Address street) may be left blank on purpose by
|
|
55
|
+
passing it with an empty value and a non-``needs-input`` origin; the cell shows ``—``.
|
|
56
|
+
* An optional field gets a column only when at least one record has a value for it.
|
|
57
|
+
* When every row's label is its ``Name``, the Name column is dropped (the Record cell shows
|
|
58
|
+
the name) so the name isn't printed twice.
|
|
59
|
+
* With more than one row, a column whose cell is the same on every row moves into one
|
|
60
|
+
``Same on every row: …`` line above the table. A ``needs-input`` column always stays.
|
|
61
|
+
* Consecutive rows whose field cells are all identical collapse into one group row
|
|
62
|
+
(``say "show all N"``); ``--expand`` lists them one by one.
|
|
63
|
+
* ``title`` replaces the default ``Records to insert — N <Object>`` heading — use it when a
|
|
64
|
+
row stands for a whole shift series, so the heading counts shifts, not rows.
|
|
65
|
+
|
|
66
|
+
Exit non-zero with a message on any spec error (unknown entity, a field the field guide
|
|
67
|
+
doesn't document, a system-managed field, a bad origin tag, a raw Id in a lookup cell, ...).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
import argparse
|
|
71
|
+
import json
|
|
72
|
+
import os
|
|
73
|
+
import re
|
|
74
|
+
import sys
|
|
75
|
+
|
|
76
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
77
|
+
DEFAULT_FIELD_GUIDE = os.path.join(os.path.dirname(HERE), "references", "field-guide.md")
|
|
78
|
+
|
|
79
|
+
ORIGINS = ("given", "default", "generated", "reuse", "new", "needs-input")
|
|
80
|
+
LOOKUP_ORIGINS = ("reuse", "new")
|
|
81
|
+
NEEDS_INPUT = "needs-input"
|
|
82
|
+
BLANK = "—"
|
|
83
|
+
# Tiers 4 and 5 are system-managed or belong to another feature — never written.
|
|
84
|
+
WRITABLE_TIERS = (1, 2, 3)
|
|
85
|
+
|
|
86
|
+
ENTITY_HEADING = re.compile(r"^###\s+(\w+)\s+—")
|
|
87
|
+
RELATIONSHIP_CELL = re.compile(r"^`(\w+)`\s*\(`(\w+)`\)")
|
|
88
|
+
BACKTICKED = re.compile(r"`(\w+)`")
|
|
89
|
+
# A 15- or 18-character record Id: alphanumeric with several digits. Names rarely look
|
|
90
|
+
# like this; Ids almost always do.
|
|
91
|
+
RECORD_ID = re.compile(r"^[A-Za-z0-9]{15}(?:[A-Za-z0-9]{3})?$")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def fail(msg):
|
|
95
|
+
sys.stderr.write("render-review-table: %s\n" % msg)
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def spell_out(name):
|
|
100
|
+
"""``ParentVolunteerInitiative`` -> ``Parent Volunteer Initiative``."""
|
|
101
|
+
if name.endswith("__c"):
|
|
102
|
+
name = name[:-3]
|
|
103
|
+
name = name.replace("_", " ")
|
|
104
|
+
return re.sub(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", " ", name).strip()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def plural(label):
|
|
108
|
+
if label.endswith("Hours"):
|
|
109
|
+
return label
|
|
110
|
+
if label.endswith("s"):
|
|
111
|
+
return label + "es"
|
|
112
|
+
if label.endswith("y") and label[-2:-1] not in "aeiou":
|
|
113
|
+
return label[:-1] + "ies"
|
|
114
|
+
return label + "s"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_field_guide(path):
|
|
118
|
+
"""Return {entity: [field, ...]} in field-guide row order.
|
|
119
|
+
|
|
120
|
+
Each field is a dict: api, name (the field guide's display name), tier, lookup,
|
|
121
|
+
schema_required, order.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
with open(path, encoding="utf-8") as fh:
|
|
125
|
+
lines = fh.read().splitlines()
|
|
126
|
+
except OSError as e:
|
|
127
|
+
fail("cannot read the field guide at %s: %s" % (path, e))
|
|
128
|
+
entities = {}
|
|
129
|
+
current = None
|
|
130
|
+
for line in lines:
|
|
131
|
+
heading = ENTITY_HEADING.match(line)
|
|
132
|
+
if heading:
|
|
133
|
+
current = heading.group(1)
|
|
134
|
+
entities[current] = []
|
|
135
|
+
continue
|
|
136
|
+
if line.startswith("## "):
|
|
137
|
+
current = None
|
|
138
|
+
continue
|
|
139
|
+
if current is None or not line.startswith("| `"):
|
|
140
|
+
continue
|
|
141
|
+
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
|
142
|
+
if len(cells) < 4:
|
|
143
|
+
continue
|
|
144
|
+
field_cell, type_cell, req_cell, tier_cell = cells[:4]
|
|
145
|
+
tier_match = re.search(r"T(\d)", tier_cell)
|
|
146
|
+
if not tier_match:
|
|
147
|
+
continue
|
|
148
|
+
tier = int(tier_match.group(1))
|
|
149
|
+
lookup = bool(re.search(r"(?i)lookup|master-detail", type_cell))
|
|
150
|
+
schema_required = req_cell.strip("* ").lower() == "yes"
|
|
151
|
+
rel = RELATIONSHIP_CELL.match(field_cell)
|
|
152
|
+
if rel:
|
|
153
|
+
pairs = [(rel.group(2), rel.group(1))]
|
|
154
|
+
else:
|
|
155
|
+
pairs = [(name, name) for name in BACKTICKED.findall(field_cell)]
|
|
156
|
+
for api, name in pairs:
|
|
157
|
+
entities[current].append({
|
|
158
|
+
"api": api, "name": name, "tier": tier, "lookup": lookup,
|
|
159
|
+
"schema_required": schema_required, "order": len(entities[current]),
|
|
160
|
+
})
|
|
161
|
+
return entities
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def resolve_columns(spec, guide):
|
|
165
|
+
entity = spec.get("entity")
|
|
166
|
+
if entity not in guide:
|
|
167
|
+
fail("unknown entity %r — use one documented in field-guide.md (%s)"
|
|
168
|
+
% (entity, ", ".join(sorted(guide))))
|
|
169
|
+
fields = guide[entity]
|
|
170
|
+
by_key = {}
|
|
171
|
+
for f in fields:
|
|
172
|
+
by_key[f["api"]] = f
|
|
173
|
+
by_key[f["name"]] = f
|
|
174
|
+
return entity, fields, by_key
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def cell_text(value):
|
|
178
|
+
if isinstance(value, bool):
|
|
179
|
+
return "Yes" if value else "No"
|
|
180
|
+
text = str(value)
|
|
181
|
+
return text.replace("|", "\\|").replace("\n", " ")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def is_empty(value):
|
|
185
|
+
return value is None or (isinstance(value, str) and not value.strip())
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def normalize_record(index, record, by_key, custom_required):
|
|
189
|
+
"""Return (row_label, {api: (value, origin)}, custom_apis) for one record."""
|
|
190
|
+
if not isinstance(record, dict) or not isinstance(record.get("fields"), dict):
|
|
191
|
+
fail("records[%d] must be an object with a 'fields' map" % index)
|
|
192
|
+
out = {}
|
|
193
|
+
customs = []
|
|
194
|
+
for key, entry in record["fields"].items():
|
|
195
|
+
where = "records[%d].%s" % (index, key)
|
|
196
|
+
if not isinstance(entry, dict) or "origin" not in entry:
|
|
197
|
+
fail("%s must be an object with 'value' and 'origin'" % where)
|
|
198
|
+
origin = entry["origin"]
|
|
199
|
+
if origin not in ORIGINS:
|
|
200
|
+
fail("%s origin %r is not one of %s" % (where, origin, ", ".join(ORIGINS)))
|
|
201
|
+
value = entry.get("value")
|
|
202
|
+
field = by_key.get(key)
|
|
203
|
+
if field is None:
|
|
204
|
+
if not key.endswith("__c"):
|
|
205
|
+
fail("%s is not in field-guide.md for this entity — never assume a field "
|
|
206
|
+
"exists (custom fields end in __c)" % where)
|
|
207
|
+
api = key
|
|
208
|
+
customs.append(api)
|
|
209
|
+
lookup = False
|
|
210
|
+
else:
|
|
211
|
+
api = field["api"]
|
|
212
|
+
lookup = field["lookup"]
|
|
213
|
+
if field["tier"] not in WRITABLE_TIERS:
|
|
214
|
+
fail("%s is system-managed or owned by another feature (tier T%d) — never "
|
|
215
|
+
"set it" % (where, field["tier"]))
|
|
216
|
+
if api in out:
|
|
217
|
+
fail("%s duplicates a field already given for this record" % where)
|
|
218
|
+
if origin != NEEDS_INPUT and not is_empty(value):
|
|
219
|
+
if lookup and origin not in LOOKUP_ORIGINS:
|
|
220
|
+
fail("%s is a lookup — tag it 'reuse' (existing record) or 'new' (created "
|
|
221
|
+
"this run)" % where)
|
|
222
|
+
if not lookup and origin in LOOKUP_ORIGINS:
|
|
223
|
+
fail("%s is not a lookup — 'reuse'/'new' tags are only for lookup cells"
|
|
224
|
+
% where)
|
|
225
|
+
if lookup and isinstance(value, str) and RECORD_ID.match(value) \
|
|
226
|
+
and sum(ch.isdigit() for ch in value) >= 3:
|
|
227
|
+
fail("%s looks like a record Id — show the referenced record's Name; Ids "
|
|
228
|
+
"appear only in the final insert report" % where)
|
|
229
|
+
out[api] = (value, origin)
|
|
230
|
+
label = record.get("label")
|
|
231
|
+
if is_empty(label):
|
|
232
|
+
name = out.get("Name", (None, None))[0]
|
|
233
|
+
label = name if not is_empty(name) else ""
|
|
234
|
+
return str(label), out, customs
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def build_table(spec, guide, expand=False):
|
|
238
|
+
entity, fields, by_key = resolve_columns(spec, guide)
|
|
239
|
+
records = spec.get("records")
|
|
240
|
+
if not isinstance(records, list) or not records:
|
|
241
|
+
fail("records must be a non-empty list")
|
|
242
|
+
labels = spec.get("labels") or {}
|
|
243
|
+
if not isinstance(labels, dict):
|
|
244
|
+
fail("labels must be a map of API name -> label")
|
|
245
|
+
custom_required = spec.get("requiredCustomFields") or []
|
|
246
|
+
if not isinstance(custom_required, list) or any(
|
|
247
|
+
not isinstance(c, str) or not c.endswith("__c") for c in custom_required):
|
|
248
|
+
fail("requiredCustomFields must be a list of custom field API names ending in __c")
|
|
249
|
+
|
|
250
|
+
rows = []
|
|
251
|
+
custom_seen = []
|
|
252
|
+
for i, rec in enumerate(records):
|
|
253
|
+
label, values, customs = normalize_record(i, rec, by_key, custom_required)
|
|
254
|
+
rows.append((label, values))
|
|
255
|
+
for c in customs:
|
|
256
|
+
if c not in custom_seen:
|
|
257
|
+
custom_seen.append(c)
|
|
258
|
+
for c in custom_required:
|
|
259
|
+
if c not in custom_seen:
|
|
260
|
+
custom_seen.append(c)
|
|
261
|
+
|
|
262
|
+
def has_value(api):
|
|
263
|
+
return any(api in v and v[api][1] != NEEDS_INPUT for _, v in rows)
|
|
264
|
+
|
|
265
|
+
def required(f):
|
|
266
|
+
return f["schema_required"] or f["tier"] == 1
|
|
267
|
+
|
|
268
|
+
writable = [f for f in fields if f["tier"] in WRITABLE_TIERS]
|
|
269
|
+
plain = sorted((f for f in writable if not f["lookup"]),
|
|
270
|
+
key=lambda f: (f["tier"], f["order"]))
|
|
271
|
+
lookups = sorted((f for f in writable if f["lookup"]),
|
|
272
|
+
key=lambda f: (f["tier"], f["order"]))
|
|
273
|
+
columns = [] # (api, header, required, schema_required)
|
|
274
|
+
for f in plain:
|
|
275
|
+
if required(f) or has_value(f["api"]):
|
|
276
|
+
columns.append((f["api"], labels.get(f["api"]) or spell_out(f["name"]),
|
|
277
|
+
required(f), f["schema_required"]))
|
|
278
|
+
for c in custom_seen:
|
|
279
|
+
if c in custom_required or has_value(c):
|
|
280
|
+
columns.append((c, labels.get(c) or spell_out(c), c in custom_required,
|
|
281
|
+
c in custom_required))
|
|
282
|
+
for f in lookups:
|
|
283
|
+
if required(f) or has_value(f["api"]):
|
|
284
|
+
columns.append((f["api"], labels.get(f["api"]) or spell_out(f["name"]),
|
|
285
|
+
required(f), f["schema_required"]))
|
|
286
|
+
|
|
287
|
+
missing = {} # header -> [row numbers]
|
|
288
|
+
|
|
289
|
+
def render_cell(row_no, values, api, header, is_required, schema_required):
|
|
290
|
+
entry = values.get(api)
|
|
291
|
+
if entry is None or entry[1] == NEEDS_INPUT:
|
|
292
|
+
if is_required:
|
|
293
|
+
missing.setdefault(header, []).append(row_no)
|
|
294
|
+
return "`%s`" % NEEDS_INPUT
|
|
295
|
+
return BLANK
|
|
296
|
+
value, origin = entry
|
|
297
|
+
if is_empty(value):
|
|
298
|
+
if schema_required:
|
|
299
|
+
missing.setdefault(header, []).append(row_no)
|
|
300
|
+
return "`%s`" % NEEDS_INPUT
|
|
301
|
+
return BLANK
|
|
302
|
+
# Only a reused lookup is tagged: it attaches the new records to data already in the
|
|
303
|
+
# org, which the user should notice. Every other value is reviewed the same way
|
|
304
|
+
# whatever its origin, so its tag would only widen the row.
|
|
305
|
+
if origin == "reuse":
|
|
306
|
+
return "%s `reuse`" % cell_text(value)
|
|
307
|
+
return cell_text(value)
|
|
308
|
+
|
|
309
|
+
body = []
|
|
310
|
+
for n, (label, values) in enumerate(rows, 1):
|
|
311
|
+
cells = [render_cell(n, values, *col) for col in columns]
|
|
312
|
+
body.append((n, label, cells))
|
|
313
|
+
|
|
314
|
+
# The row label already shows the Name, so a Name column would repeat it. The label
|
|
315
|
+
# still keeps records with different names from grouping.
|
|
316
|
+
name_dropped = False
|
|
317
|
+
if "Name" in [c[0] for c in columns] and all(
|
|
318
|
+
"Name" in v and v["Name"][1] != NEEDS_INPUT and not is_empty(v["Name"][0])
|
|
319
|
+
and str(v["Name"][0]) == label for label, v in rows):
|
|
320
|
+
name_dropped = True
|
|
321
|
+
idx = [c[0] for c in columns].index("Name")
|
|
322
|
+
columns.pop(idx)
|
|
323
|
+
for _, _, cells in body:
|
|
324
|
+
cells.pop(idx)
|
|
325
|
+
|
|
326
|
+
# A column with the same value on every row is stated once above the table instead of
|
|
327
|
+
# widening every row. A needs-input column stays so the gap is visible on its rows.
|
|
328
|
+
shared = []
|
|
329
|
+
if len(body) > 1:
|
|
330
|
+
keep = []
|
|
331
|
+
for i, col in enumerate(columns):
|
|
332
|
+
first = body[0][2][i]
|
|
333
|
+
if first != "`%s`" % NEEDS_INPUT and all(b[2][i] == first for b in body):
|
|
334
|
+
shared.append("%s %s" % (col[1], first))
|
|
335
|
+
else:
|
|
336
|
+
keep.append(i)
|
|
337
|
+
if shared:
|
|
338
|
+
columns = [columns[i] for i in keep]
|
|
339
|
+
body = [(n, label, [cells[i] for i in keep]) for n, label, cells in body]
|
|
340
|
+
|
|
341
|
+
lines = []
|
|
342
|
+
entity_label = spell_out(entity)
|
|
343
|
+
title = spec.get("title")
|
|
344
|
+
if title is not None and (not isinstance(title, str) or not title.strip()):
|
|
345
|
+
fail("title must be a non-empty string")
|
|
346
|
+
lines.append(title or "Records to insert — %d %s"
|
|
347
|
+
% (len(rows), entity_label if len(rows) == 1 else plural(entity_label)))
|
|
348
|
+
lines.append("")
|
|
349
|
+
if shared:
|
|
350
|
+
lines.append("Same on every row: " + " · ".join(shared))
|
|
351
|
+
lines.append("")
|
|
352
|
+
headers = ["Record"] + [c[1] for c in columns]
|
|
353
|
+
lines.append("| " + " | ".join(headers) + " |")
|
|
354
|
+
lines.append("|" + "|".join("---" for _ in headers) + "|")
|
|
355
|
+
|
|
356
|
+
def record_cell(n, label):
|
|
357
|
+
return "%d · %s" % (n, cell_text(label)) if label else str(n)
|
|
358
|
+
|
|
359
|
+
def same_row(a, b):
|
|
360
|
+
return a[2] == b[2] and (not name_dropped or a[1] == b[1])
|
|
361
|
+
|
|
362
|
+
i = 0
|
|
363
|
+
while i < len(body):
|
|
364
|
+
j = i
|
|
365
|
+
while not expand and j + 1 < len(body) and same_row(body[j + 1], body[i]):
|
|
366
|
+
j += 1
|
|
367
|
+
if j > i:
|
|
368
|
+
size = j - i + 1
|
|
369
|
+
n_first, label, cells = body[i]
|
|
370
|
+
first = "Rows %d–%d" % (n_first, body[j][0])
|
|
371
|
+
if label:
|
|
372
|
+
first += " · %s" % cell_text(label)
|
|
373
|
+
first += " (%d identical — say \"show all %d\")" % (size, size)
|
|
374
|
+
lines.append("| " + " | ".join([first] + cells) + " |")
|
|
375
|
+
else:
|
|
376
|
+
n, label, cells = body[i]
|
|
377
|
+
lines.append("| " + " | ".join([record_cell(n, label)] + cells) + " |")
|
|
378
|
+
i = j + 1
|
|
379
|
+
|
|
380
|
+
rendered = "\n".join(lines)
|
|
381
|
+
notes = []
|
|
382
|
+
if "`reuse`" in rendered:
|
|
383
|
+
notes.append("`reuse` = links to a record already in your org")
|
|
384
|
+
if missing:
|
|
385
|
+
notes.append("`needs-input` = required, still missing")
|
|
386
|
+
lines.append("")
|
|
387
|
+
if notes:
|
|
388
|
+
lines.append(" · ".join(notes))
|
|
389
|
+
if missing:
|
|
390
|
+
parts = ["%s (row%s %s)" % (h, "s" if len(r) > 1 else "", ", ".join(map(str, r)))
|
|
391
|
+
for h, r in missing.items()]
|
|
392
|
+
lines.append("Still needed before this can be written: " + "; ".join(parts))
|
|
393
|
+
return "\n".join(lines) + "\n"
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def main():
|
|
397
|
+
ap = argparse.ArgumentParser(description="Render a VM records review table.")
|
|
398
|
+
ap.add_argument("--expand", action="store_true",
|
|
399
|
+
help="list identical rows one by one instead of grouping them")
|
|
400
|
+
ap.add_argument("--field-guide", default=DEFAULT_FIELD_GUIDE,
|
|
401
|
+
help="path to field-guide.md (default: the skill's references copy)")
|
|
402
|
+
args = ap.parse_args()
|
|
403
|
+
|
|
404
|
+
raw = sys.stdin.read()
|
|
405
|
+
if not raw.strip():
|
|
406
|
+
fail("no spec on stdin — pipe a JSON spec (see the module docstring)")
|
|
407
|
+
try:
|
|
408
|
+
spec = json.loads(raw)
|
|
409
|
+
except json.JSONDecodeError as e:
|
|
410
|
+
fail("spec is not valid JSON: %s" % e)
|
|
411
|
+
if not isinstance(spec, dict):
|
|
412
|
+
fail("spec must be a JSON object (got %s)" % type(spec).__name__)
|
|
413
|
+
|
|
414
|
+
guide = parse_field_guide(args.field_guide)
|
|
415
|
+
sys.stdout.write(build_table(spec, guide, expand=args.expand))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if __name__ == "__main__":
|
|
419
|
+
main()
|