@salesforce/afv-skills 1.55.0 → 1.57.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-generate/SKILL.md +8 -0
- package/skills/agentforce-generate/references/actions-reference.md +2 -2
- package/skills/agentforce-generate/references/voice-modality-reference.md +51 -15
- package/skills/agentforce-generate/references/voice-telephony-cli.md +193 -0
- package/skills/agentforce-observe/SKILL.md +13 -9
- package/skills/agentforce-observe/references/ahm-alerts.md +193 -14
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +24 -15
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +11 -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 +32 -1
- package/skills/automation-sandbox-post-copy-configure/SKILL.md +75 -71
- package/skills/automation-sandbox-post-copy-configure/assets/scheduled_apex_template.apex +13 -0
- package/skills/automation-sandbox-post-copy-configure/examples/sample_scheduled_apex_config.json +13 -0
- package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +155 -0
- package/skills/automation-sandbox-post-copy-configure/scripts/build-scheduled-apex.mjs +62 -0
- package/skills/automation-sandbox-post-copy-configure/scripts/soql-escape-job-name.mjs +28 -0
- package/skills/commerce-b2b-open-code-components-integrate/SKILL.md +1 -1
- package/skills/commerce-b2b-open-code-components-replace/SKILL.md +16 -16
- package/skills/consumer-goods-accruals-datakit-deploy/SKILL.md +383 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +324 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +63 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +100 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +46 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/04-deploy-engine.js +198 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/05-deploy-tpm-accruals.js +231 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +82 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +517 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +416 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +514 -0
- package/skills/consumer-goods-sync-management-configure/SKILL.md +137 -0
- package/skills/consumer-goods-sync-management-configure/examples/scenarios.md +294 -0
- package/skills/consumer-goods-sync-management-configure/references/act1-setup-sync.md +333 -0
- package/skills/consumer-goods-sync-management-configure/references/act2-assign-users.md +324 -0
- package/skills/consumer-goods-sync-management-configure/references/act3-plan-verify.md +113 -0
- package/skills/consumer-goods-sync-management-configure/references/readiness-and-enablement.md +164 -0
- package/skills/consumer-goods-sync-management-configure/references/sync-management-app-install-backbone.md +159 -0
- package/skills/consumer-goods-sync-management-configure/references/transports-and-namespace.md +220 -0
- package/skills/consumer-goods-sync-management-configure/references/verify-and-smoke.md +175 -0
- package/skills/dx-devops-project-manage/SKILL.md +1 -1
- package/skills/dx-org-analyze/SKILL.md +0 -1
- package/skills/dx-org-analyze/scripts/collect_org_data.py +2 -2
- package/skills/dx-org-analyze/scripts/compute_diff.py +2 -4
- package/skills/dx-org-analyze/scripts/introspect_org.py +4 -40
- package/skills/dx-org-manage/SKILL.md +6 -50
- package/skills/dx-org-manage/examples/README.md +10 -16
- package/skills/dx-org-manage/references/scratch-org-create.md +1 -1
- package/skills/dx-org-manage/references/scratch-org-operations.md +1 -1
- package/skills/dx-org-shape-manage/SKILL.md +4 -3
- package/skills/education-cloud-domain-configure/references/gotchas-extended.md +1 -0
- package/skills/experience-accessibility-validate/SKILL.md +8 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-1-1-non-text-content.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-i-lists.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-ii-tables.md +20 -85
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iii-form-labels.md +4 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iv-regions.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-v-groups.md +2 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-5-identify-input.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-4-3-contrast.md +4 -14
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-1-1-keyboard.md +9 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-4-link-purpose.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-6-headings-labels.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-1-pointer-gestures.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-2-pointer-cancellation.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-3-label-in-name.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-7-dragging-movement.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-1-on-focus.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-2-on-input.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-1-error-identification.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-2-labels-instructions.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-3-error-suggestion.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-i-name.md +4 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-ii-role.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-iii-value.md +0 -4
- package/skills/experience-cms-content-generate/SKILL.md +1 -1
- package/skills/experience-cms-content-render/SKILL.md +3 -3
- package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +35 -7
- package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +13 -4
- package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +30 -8
- package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +14 -4
- package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +11 -1
- package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +1 -1
- package/skills/experience-cms-content-render/assets/shared/mediaLabels.ts +62 -0
- package/skills/experience-cms-content-render/references/init-scaffold.md +13 -6
- package/skills/experience-content-media-stock-image-search/SKILL.md +0 -1
- package/skills/experience-lwc-accessibility-jest-run/SKILL.md +104 -25
- package/skills/experience-lwc-accessibility-jest-run/assets/jest.config.js +4 -0
- package/skills/experience-lwc-accessibility-jest-run/assets/sa11y-jest-setup.js +5 -0
- package/skills/experience-lwc-design-generate/SKILL.md +1 -1
- package/skills/experience-lwc-legacy-migrate/SKILL.md +212 -0
- package/skills/experience-lwc-legacy-migrate/assets/lo20-host-page-template.html +192 -0
- package/skills/experience-lwc-legacy-migrate/references/aura-to-lwc-completeness-checklist.md +188 -0
- package/skills/experience-lwc-legacy-migrate/references/lightning-out-2-system-reference.md +495 -0
- package/skills/experience-lwc-legacy-migrate/references/lightning-out-beta-to-2-migration.md +824 -0
- package/skills/experience-lwc-legacy-migrate/scripts/convert-lo-names.py +94 -0
- package/skills/experience-lwc-legacy-migrate/scripts/validate-lo20-page.py +133 -0
- package/skills/experience-ui-bundle-app-coordinate/SKILL.md +5 -5
- package/skills/experience-ui-bundle-features-generate/SKILL.md +65 -36
- package/skills/experience-ui-bundle-features-generate/references/angular/features.md +74 -0
- package/skills/experience-ui-bundle-features-generate/references/react/features.md +59 -0
- package/skills/experience-ui-bundle-features-generate/scripts/detect-framework.sh +73 -0
- package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -2
- package/skills/experience-ui-bundle-metadata-generate/SKILL.md +26 -14
- package/skills/experience-ui-bundle-metadata-generate/references/angular-metadata-generate.md +37 -0
- package/skills/experience-ui-bundle-metadata-generate/references/react-metadata-generate.md +32 -0
- package/skills/experience-ui-bundle-metadata-generate/scripts/detect-framework.sh +73 -0
- package/skills/experience-ui-bundle-metadata-generate/scripts/verify-bundle-location.sh +30 -8
- package/skills/experience-ui-bundle-project-generate/SKILL.md +1 -1
- package/skills/field-service-data-capture-migrate/SKILL.md +391 -0
- package/skills/field-service-data-capture-migrate/assets/sample-legacy-flow.flow-meta.xml +163 -0
- package/skills/field-service-data-capture-migrate/references/architecture-notes.md +40 -0
- package/skills/field-service-data-capture-migrate/references/component-mapping.md +278 -0
- package/skills/field-service-data-capture-migrate/references/converter-builder-contract.md +183 -0
- package/skills/field-service-data-capture-migrate/references/dc-platform-rules.md +130 -0
- package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +215 -0
- package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +36 -0
- package/skills/field-service-data-capture-migrate/references/migration-checklist.md +121 -0
- package/skills/field-service-data-capture-migrate/references/transformer-rules.md +132 -0
- package/skills/field-service-data-capture-migrate/scripts/analyze_flow.py +277 -0
- package/skills/field-service-data-capture-migrate/scripts/convert_to_dc_spec.py +1311 -0
- package/skills/field-service-data-capture-migrate/scripts/cud_analysis.py +402 -0
- package/skills/field-service-data-capture-migrate/scripts/deploy_flow.sh +230 -0
- package/skills/field-service-data-capture-migrate/scripts/discover_subflow_tree.py +230 -0
- package/skills/field-service-data-capture-migrate/scripts/flow_input.py +186 -0
- package/skills/field-service-data-capture-migrate/scripts/flow_xml_utils.py +39 -0
- package/skills/field-service-data-capture-migrate/scripts/fsm_dc_catalog.py +413 -0
- package/skills/field-service-data-capture-migrate/scripts/generate_summary.py +199 -0
- package/skills/field-service-data-capture-migrate/scripts/json_to_flow_xml.py +146 -0
- package/skills/field-service-data-capture-migrate/scripts/manual_review_flags.py +42 -0
- package/skills/field-service-data-capture-migrate/scripts/retrieve_flow_rest.sh +155 -0
- package/skills/field-service-data-capture-migrate/scripts/subflow_names.py +49 -0
- package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +1661 -0
- package/skills/field-service-data-capture-migrate/scripts/validate_flow.sh +147 -0
- package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +1258 -0
- package/skills/integration-connectivity-connected-app-configure/SKILL.md +5 -5
- package/skills/integration-connectivity-generate/SKILL.md +10 -10
- package/skills/integration-connectivity-generate/scripts/configure-named-credential.sh +2 -2
- package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +1 -1
- package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +1 -1
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +14 -1
- package/skills/mobile-apps-create/SKILL.md +1 -1
- package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/SKILL.md +7 -7
- package/skills/platform-apex-test-generate/SKILL.md +1 -1
- package/skills/platform-apex-test-run/SKILL.md +1 -1
- package/skills/platform-custom-field-generate/SKILL.md +1 -1
- package/skills/platform-custom-lightning-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-metadata-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-object-generate/SKILL.md +1 -1
- package/skills/platform-custom-report-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-setting-generate/SKILL.md +1 -1
- package/skills/platform-data-and-tooling-api-context-get/SKILL.md +1 -1
- package/skills/platform-data-manage/SKILL.md +10 -10
- package/skills/platform-flexipage-generate/SKILL.md +1 -1
- package/skills/platform-lightning-app-coordinate/SKILL.md +1 -1
- package/skills/platform-metadata-api-context-get/SKILL.md +1 -1
- package/skills/platform-metadata-retrieve/SKILL.md +1 -1
- package/skills/platform-permission-set-generate/SKILL.md +1 -1
- package/skills/platform-report-generate/SKILL.md +1 -1
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +4 -4
- package/skills/platform-sandbox-configure/SKILL.md +108 -133
- package/skills/platform-sandbox-configure/references/api-response-shapes.md +39 -0
- package/skills/platform-sandbox-configure/references/definition-file-approach.md +73 -0
- package/skills/platform-sharing-owd-configure/SKILL.md +1 -1
- package/skills/platform-sharing-rules-generate/SKILL.md +1 -1
- package/skills/platform-soql-query/SKILL.md +1 -1
- package/skills/platform-value-set-generate/SKILL.md +1 -1
- package/skills/platform-widget-generate/SKILL.md +26 -18
- package/skills/platform-widget-generate/examples/formulas.json +191 -0
- package/skills/platform-widget-generate/references/widget-formulas.md +123 -0
- package/skills/platform-widget-generate/references/widget-meta-directives.md +4 -4
- package/skills/sales-call-scoring-configure/SKILL.md +318 -0
- package/skills/sales-call-scoring-configure/assets/best-practice-competencies.json +67 -0
- package/skills/sales-call-scoring-configure/assets/ootb-competencies.json +58 -0
- package/skills/sales-call-scoring-configure/references/admin-communication.md +79 -0
- package/skills/sales-call-scoring-configure/references/competency-crud.md +130 -0
- package/skills/sales-call-scoring-configure/scripts/create-competency.sh +166 -0
- package/skills/sales-call-scoring-configure/scripts/create-custom-competency.sh +166 -0
- package/skills/sales-call-scoring-configure/scripts/edit-competency.sh +185 -0
- package/skills/sales-call-scoring-configure/scripts/edit-custom-competency.sh +190 -0
- package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +436 -0
- package/skills/sales-call-scoring-configure/scripts/get-competency.sh +72 -0
- package/skills/sales-call-scoring-configure/scripts/get-custom-competency.sh +78 -0
- package/skills/sales-call-scoring-configure/scripts/install-best-practice-competencies.sh +223 -0
- package/skills/sales-call-scoring-configure/scripts/install-ootb-competencies.sh +237 -0
- package/skills/sales-call-scoring-configure/scripts/list-competencies.sh +99 -0
- package/skills/sales-call-scoring-configure/scripts/shared/auth.sh +116 -0
- package/skills/sales-call-scoring-configure/scripts/shared/cap.sh +86 -0
- package/skills/sales-call-scoring-configure/scripts/shared/soap.sh +267 -0
- package/skills/sales-call-scoring-configure/scripts/shared/soql.sh +25 -0
- package/skills/sales-call-scoring-configure/scripts/toggle-competency.sh +112 -0
- package/skills/sales-call-scoring-configure/scripts/toggle-custom-competency.sh +125 -0
- package/skills/sales-call-scoring-configure/scripts/verify-ootb-competencies.sh +116 -0
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +4 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +26 -6
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +5 -4
- package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1 -0
- package/skills/service-de-channel-activate/SKILL.md +6 -5
- package/skills/service-de-channel-create/SKILL.md +3 -3
- package/skills/service-de-channel-routing-configure/SKILL.md +3 -3
- package/skills/service-de-channel-settings-configure/SKILL.md +195 -0
- package/skills/service-de-channel-settings-configure/references/channel-settings-facets.md +152 -0
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/gotchas.md +3 -3
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/language-keywords.md +4 -4
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/worked-examples.md +14 -14
- package/skills/service-de-headless-channel-configure/SKILL.md +6 -6
- package/skills/service-de-headless-channel-configure/references/worked-examples.md +1 -1
- package/skills/service-de-waba-integrate/SKILL.md +2 -1
- package/skills/service-digital-engagement-deployment-configure/SKILL.md +2 -0
- package/skills/service-digital-engagement-deployment-configure/scripts/check-api-version.sh +29 -0
- package/skills/service-email-to-case-configure/SKILL.md +1 -1
- package/skills/service-helpagent-coordinate/SKILL.md +3 -2
- package/skills/service-helpagent-coordinate/references/channel-voice.md +15 -4
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +49 -31
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +71 -6
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +26 -4
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +35 -10
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/references/mcp-invocation.md +25 -5
- package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +5 -5
- package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +2 -2
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/examples/output-templates.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +25 -15
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -5
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +4 -2
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +1 -1
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +29 -16
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -5
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +4 -2
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +1 -1
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +29 -16
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +138 -68
- package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +31 -15
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +156 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +148 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +162 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +149 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +103 -89
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +2 -2
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +21 -39
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +3 -3
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +18 -1
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +63 -37
- package/skills/service-omni-attribute-routing-configure/SKILL.md +53 -0
- package/skills/service-omni-attribute-routing-configure/references/api-notes.md +7 -0
- package/skills/service-omni-attribute-routing-configure/scripts/configure-and-report.sh +224 -0
- package/skills/service-omni-attribute-routing-configure/scripts/tests/test_attribute_routing_contracts.py +137 -0
- package/skills/service-omni-channel-inventory-analyze/SKILL.md +96 -0
- package/skills/service-omni-channel-inventory-analyze/references/api-notes.md +52 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/analyze.sh +185 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/tests/_bootstrap.py +84 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/tests/test_inventory_analyze_contracts.py +141 -0
- package/skills/service-omni-channel-limits-analyze/SKILL.md +70 -0
- package/skills/service-omni-channel-limits-analyze/references/api-notes.md +22 -0
- package/skills/service-omni-channel-limits-analyze/scripts/analyze.sh +159 -0
- package/skills/service-omni-channel-limits-analyze/scripts/tests/_bootstrap.py +87 -0
- package/skills/service-omni-channel-limits-analyze/scripts/tests/test_limits_analyze_contracts.py +99 -0
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +12 -20
- package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +55 -5
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +38 -0
- package/skills/service-omni-command-center-analyze/SKILL.md +2 -2
- package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
- package/skills/dx-org-manage/examples/snapshots/error_output.json +0 -9
- package/skills/dx-org-manage/examples/snapshots/success_output.json +0 -15
- package/skills/dx-org-manage/references/cli_flags.md +0 -67
- package/skills/dx-org-manage/references/creating-snapshot.md +0 -103
- package/skills/dx-org-manage/references/snapshot_usage.md +0 -74
- package/skills/experience-accessibility-validate/scripts/contrast-ratio.py +0 -153
- package/skills/experience-lwc-accessibility-jest-run/references/running-sa11y-jest-tests.md +0 -205
- package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +0 -14
- /package/skills/experience-ui-bundle-features-generate/references/{conflict-resolution-schema.json → common/conflict-resolution-schema.json} +0 -0
- /package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/references/api-response-schema.md +0 -0
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Convert intermediate Data Capture Flow JSON into Salesforce Flow XML.
|
|
3
|
+
|
|
4
|
+
Schema is derived from real DataCaptureFlows retrieved from a Field Service
|
|
5
|
+
demo org (Data_Capture_Asset_Inspection, Inventory_Transfer). Notes on what
|
|
6
|
+
is verified vs guessed live in TYPE_MAP below.
|
|
7
|
+
|
|
8
|
+
Input JSON shape:
|
|
9
|
+
{
|
|
10
|
+
"formTitle": "...",
|
|
11
|
+
"formType": "...",
|
|
12
|
+
"screens": {
|
|
13
|
+
"PascalCaseName": [ { fieldName, fieldLabel, fieldType, isRequired,
|
|
14
|
+
defaultValue, options,
|
|
15
|
+
[repeaterFields], [matrixRows],
|
|
16
|
+
[min, max, value],
|
|
17
|
+
[visibility: { field, operator, value }] }, ... ],
|
|
18
|
+
...
|
|
19
|
+
},
|
|
20
|
+
"postScreen": { # optional
|
|
21
|
+
"variables": [ { name, type } ],
|
|
22
|
+
"decision": { name, label, defaultLabel, branches: [...] },
|
|
23
|
+
"recordLookups": [ { name, label, object, filters, outputAssignments, next } ],
|
|
24
|
+
"loop": { name, label, collection, next },
|
|
25
|
+
"recordCreates": [ { name, label, object, inputAssignments, next } ]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
build_flow.py <input.json> <FlowApiName> <output.flow-meta.xml>
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
import re
|
|
37
|
+
import sys
|
|
38
|
+
from dataclasses import dataclass, field as dc_field
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any
|
|
41
|
+
from xml.sax.saxutils import escape as xml_escape
|
|
42
|
+
|
|
43
|
+
API_VERSION = "65.0"
|
|
44
|
+
NS = "http://soap.sforce.com/2006/04/metadata"
|
|
45
|
+
|
|
46
|
+
# ---- Field-type mapping --------------------------------------------------
|
|
47
|
+
#
|
|
48
|
+
# Each entry: extension_name, render_strategy, [optional] fallback_label_prefix
|
|
49
|
+
#
|
|
50
|
+
# render_strategy values:
|
|
51
|
+
# "instance" -> <fieldType>ComponentInstance</fieldType>, label via inputParameters
|
|
52
|
+
# "choice" -> <fieldType>ComponentChoice</fieldType>, single-select choices
|
|
53
|
+
# "multichoice" -> <fieldType>ComponentMultiChoice</fieldType>, multi-select choices
|
|
54
|
+
# "displaytext" -> native <fieldType>DisplayText</fieldType>, no extension
|
|
55
|
+
# "repeater" -> native <fieldType>Repeater</fieldType>, nested <fields>
|
|
56
|
+
#
|
|
57
|
+
# All entries below have been verified against retrieved flows from the
|
|
58
|
+
# fs-demo org (May 2026) unless tagged PROBE.
|
|
59
|
+
|
|
60
|
+
VERIFIED = "verified"
|
|
61
|
+
PROBE = "probe"
|
|
62
|
+
|
|
63
|
+
# Each entry: (extension_name, render_strategy, fallback_prefix, status, extra_inputs)
|
|
64
|
+
# extra_inputs is a list of (param_name, kind, value) auto-injected on every instance.
|
|
65
|
+
# kind="ref" -> <value><elementReference>VALUE</elementReference></value>
|
|
66
|
+
# kind="bool" -> <value><booleanValue>VALUE</booleanValue></value>
|
|
67
|
+
# kind="str" -> <value><stringValue>VALUE</stringValue></value>
|
|
68
|
+
#
|
|
69
|
+
# All component names verified against the published Field Service data capture
|
|
70
|
+
# component reference (https://help.salesforce.com/s/articleView?id=service.mfs_data_capture_components.htm)
|
|
71
|
+
# and a retrieved Data_Capture_All_Components flow from a live FS org (May 2026).
|
|
72
|
+
|
|
73
|
+
TYPE_MAP: dict[str, tuple[str, str, str | None, str, list[tuple[str, str, str]]]] = {
|
|
74
|
+
# type extension strategy fallback_prefix status extra_inputs
|
|
75
|
+
"ShortText": ("runtime_service_fieldservice:dcTextInput", "instance", None, VERIFIED, []),
|
|
76
|
+
"LongText": ("runtime_service_fieldservice:dcLongText", "instance", None, VERIFIED, []),
|
|
77
|
+
"Name": ("runtime_service_fieldservice:dcName", "instance", None, VERIFIED, []),
|
|
78
|
+
"Email": ("runtime_service_fieldservice:dcEmail", "instance", None, PROBE, []),
|
|
79
|
+
"Phone": ("runtime_service_fieldservice:dcPhone", "instance", None, PROBE, []),
|
|
80
|
+
"Numeric": ("runtime_service_fieldservice:dcNumeric", "instance", None, PROBE, []),
|
|
81
|
+
"Counter": ("runtime_service_fieldservice:dcCounter", "instance", None, VERIFIED, []),
|
|
82
|
+
"Date": ("runtime_service_fieldservice:dcDate", "instance", None, PROBE, []),
|
|
83
|
+
"DateTime": ("runtime_service_fieldservice:dcDateTime", "instance", None, PROBE, []),
|
|
84
|
+
"Checkbox": ("runtime_service_fieldservice:dcCheckbox", "instance", None, PROBE, []),
|
|
85
|
+
"Toggle": ("runtime_service_fieldservice:dcToggle", "instance", None, VERIFIED, []),
|
|
86
|
+
"Picklist": ("runtime_service_fieldservice:dcPicklist", "choice", None, VERIFIED, []),
|
|
87
|
+
# Radio uses dcRbGroup (verified May 2026 via Inventory_Transfer flow).
|
|
88
|
+
"Radio": ("runtime_service_fieldservice:dcRbGroup", "choice", None, VERIFIED, []),
|
|
89
|
+
"CheckboxGroup":("runtime_service_fieldservice:dcCbGroup", "multichoice", None, VERIFIED, []),
|
|
90
|
+
"DisplayText": ("", "displaytext", None, VERIFIED, []),
|
|
91
|
+
"Repeater": ("", "repeater", None, VERIFIED, []),
|
|
92
|
+
# Real Field Service data capture components. Their `recordId` / `parentRecordId`
|
|
93
|
+
# input parameters are auto-wired to the standard Data Capture input variables
|
|
94
|
+
# `parentRecordId` and `recordId` (declared at the bottom of every flow).
|
|
95
|
+
"Signature": ("runtime_service_fieldservice:dcSignature", "instance", None, VERIFIED, [("parentRecordId", "ref", "parentRecordId"), ("recordId", "ref", "recordId")]),
|
|
96
|
+
"UploadFile": ("runtime_service_fieldservice:dcUpFile", "instance", None, VERIFIED, [("recordId", "ref", "parentRecordId")]),
|
|
97
|
+
"UploadImage": ("runtime_service_fieldservice:dcUpImage", "instance", None, VERIFIED, [("recordId", "ref", "parentRecordId")]),
|
|
98
|
+
"Address": ("runtime_service_fieldservice:dcAddress", "instance", None, VERIFIED, []),
|
|
99
|
+
"Matrix": ("runtime_service_fieldservice:dcMatrix", "matrix", None, VERIFIED, []),
|
|
100
|
+
"Images": ("runtime_service_fieldservice:dcImages", "instance", None, VERIFIED, [("recordId", "ref", "parentRecordId")]),
|
|
101
|
+
# Lookup is a real component but needs `objectApiName` (and optionally `searchedFields`)
|
|
102
|
+
# supplied via the spec's `lookupObject` / `lookupSearchFields` keys. If `lookupObject`
|
|
103
|
+
# is missing, render_field falls back to a labeled ShortText so the flow still deploys.
|
|
104
|
+
"Lookup": ("runtime_service_fieldservice:dcLookup", "instance", None, VERIFIED, []),
|
|
105
|
+
# FileView is a read-only static-image viewer; it needs `fileName` (the asset
|
|
106
|
+
# filename without extension) supplied via the spec's `fileName` key.
|
|
107
|
+
"FileView": ("runtime_service_fieldservice:dcFileView", "instance", None, VERIFIED, []),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
API_NAME_RE = re.compile(r"^[A-Z][A-Za-z0-9_]*$")
|
|
111
|
+
MAX_SECTION_DEPTH = 5
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class Field:
|
|
116
|
+
name: str
|
|
117
|
+
label: str
|
|
118
|
+
field_type: str
|
|
119
|
+
is_required: bool
|
|
120
|
+
default_value: Any = None
|
|
121
|
+
options: list[str] | None = None
|
|
122
|
+
matrix_rows: list[str] | None = None
|
|
123
|
+
matrix_questions: list[str] | None = None # Matrix component's row labels (input parameter "questions")
|
|
124
|
+
repeater_fields: list["Field"] | None = None
|
|
125
|
+
counter_min: float | None = None
|
|
126
|
+
counter_max: float | None = None
|
|
127
|
+
counter_value: float | None = None
|
|
128
|
+
visibility: dict | None = None # { "field": str, "operator": str, "value": str }
|
|
129
|
+
# Lookup-specific:
|
|
130
|
+
lookup_object: str | None = None # objectApiName (e.g. "Asset")
|
|
131
|
+
lookup_search_fields: str | None = None # comma-sep field names (e.g. "Name, SerialNumber")
|
|
132
|
+
lookup_multi: bool = False # isMultiSelection
|
|
133
|
+
# FileView-specific:
|
|
134
|
+
file_name: str | None = None # static asset filename without extension
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class Section:
|
|
139
|
+
"""Visual grouping rendered as a Salesforce Section component
|
|
140
|
+
(RegionContainer with a single full-width Region child).
|
|
141
|
+
|
|
142
|
+
Sections are emitted only when the source explicitly shows them. Specs
|
|
143
|
+
without sections deploy as a flat list of fields exactly as before.
|
|
144
|
+
"""
|
|
145
|
+
name: str
|
|
146
|
+
label: str
|
|
147
|
+
fields: list[Field]
|
|
148
|
+
visibility: dict | None = None # shape (b): {conditionLogic, conditions:[...]}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class Screen:
|
|
153
|
+
name: str
|
|
154
|
+
label: str
|
|
155
|
+
# Either a flat list of Field, or a list of Section. Mixed is not allowed:
|
|
156
|
+
# if any item in a screen carries `section`, all fields after the first
|
|
157
|
+
# sentinel get bucketed into Sections; fields before the first sentinel go
|
|
158
|
+
# into an implicit lead-in Section labeled "General".
|
|
159
|
+
fields: list[Field]
|
|
160
|
+
sections: list[Section] | None = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class FormSpec:
|
|
165
|
+
title: str
|
|
166
|
+
form_type: str
|
|
167
|
+
screens: list[Screen]
|
|
168
|
+
# All choices used across the flow, deduped: maps choice api_name -> display_text
|
|
169
|
+
choices: dict[str, str] = dc_field(default_factory=dict)
|
|
170
|
+
# Field name (api) -> field type, used by visibility resolution
|
|
171
|
+
fields_by_name: dict[str, Field] = dc_field(default_factory=dict)
|
|
172
|
+
# Raw post-screen automation block (passes through as-is; emitted in build_xml)
|
|
173
|
+
post_screen: dict | None = None
|
|
174
|
+
decisions: list = dc_field(default_factory=list)
|
|
175
|
+
screen_connectors: dict = dc_field(default_factory=dict)
|
|
176
|
+
warnings: list[str] = dc_field(default_factory=list)
|
|
177
|
+
# WI-7: {type, name, reason} entries the converter could not fully migrate.
|
|
178
|
+
incomplete_migration: list = dc_field(default_factory=list)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def coerce_api_name(raw: str, *, fallback_prefix: str = "f_") -> str:
|
|
182
|
+
cleaned = re.sub(r"[^A-Za-z0-9_]", "_", raw or "")
|
|
183
|
+
cleaned = re.sub(r"_+", "_", cleaned).strip("_")
|
|
184
|
+
if not cleaned:
|
|
185
|
+
cleaned = "field"
|
|
186
|
+
if not cleaned[0].isalpha():
|
|
187
|
+
cleaned = fallback_prefix + cleaned
|
|
188
|
+
return cleaned[:80]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _xml_comment_safe(text: str) -> str:
|
|
192
|
+
"""Make text safe inside an XML comment: no '--' run, no comment-closing '>'."""
|
|
193
|
+
s = (text or "").replace("\r", " ").replace("\n", " ")
|
|
194
|
+
# Collapse any run of dashes to an em-dash so '--' / '-->' can't appear.
|
|
195
|
+
s = re.sub(r"-{2,}", "—", s)
|
|
196
|
+
# The '--' collapse above already prevents a premature '-->'. Escaping any
|
|
197
|
+
# remaining '>' is defensive only — a lone '>' is legal in a comment, but
|
|
198
|
+
# this keeps the body free of angle brackets.
|
|
199
|
+
s = s.replace(">", ">")
|
|
200
|
+
return " ".join(s.split())
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def humanize_screen_key(key: str) -> str:
|
|
204
|
+
s = key.replace("_", " ")
|
|
205
|
+
s = re.sub(r"(?<!^)(?=[A-Z])", " ", s)
|
|
206
|
+
return re.sub(r"\s+", " ", s).strip()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def parse_field(raw: dict, used_names: set[str], warnings: list[str] | None = None) -> Field:
|
|
210
|
+
name = (raw.get("fieldName") or "").strip()
|
|
211
|
+
if not name:
|
|
212
|
+
raise ValueError(f"field missing fieldName: {raw}")
|
|
213
|
+
api_name = uniquify(coerce_api_name(name), used_names)
|
|
214
|
+
|
|
215
|
+
original_field_type = (raw.get("fieldType") or "ShortText").strip()
|
|
216
|
+
field_type = original_field_type
|
|
217
|
+
if field_type not in TYPE_MAP:
|
|
218
|
+
field_type = "ShortText"
|
|
219
|
+
existing = (raw.get("fieldLabel") or "").strip()
|
|
220
|
+
raw["fieldLabel"] = f"[Unknown type: {original_field_type}] {existing}".strip()
|
|
221
|
+
# FIX I2: surface unknown fieldType as a warning so it appears in output.
|
|
222
|
+
if warnings is not None:
|
|
223
|
+
warnings.append(f"field '{api_name}': unknown fieldType '{original_field_type}', downgraded to ShortText")
|
|
224
|
+
|
|
225
|
+
label = (raw.get("fieldLabel") or api_name).strip()
|
|
226
|
+
is_required = bool(raw.get("isRequired"))
|
|
227
|
+
default_value = raw.get("defaultValue")
|
|
228
|
+
options_raw = raw.get("options")
|
|
229
|
+
options = [str(o) for o in options_raw] if isinstance(options_raw, list) else None
|
|
230
|
+
matrix_rows_raw = raw.get("matrixRows")
|
|
231
|
+
matrix_rows = [str(r) for r in matrix_rows_raw] if isinstance(matrix_rows_raw, list) else None
|
|
232
|
+
|
|
233
|
+
repeater_children: list[Field] | None = None
|
|
234
|
+
if field_type == "Repeater":
|
|
235
|
+
children_raw = raw.get("repeaterFields") or []
|
|
236
|
+
if not children_raw:
|
|
237
|
+
raise ValueError(f"Repeater '{api_name}' has no repeaterFields")
|
|
238
|
+
repeater_children = [parse_field(c, used_names, warnings) for c in children_raw]
|
|
239
|
+
|
|
240
|
+
counter_min = _to_number(raw.get("min"))
|
|
241
|
+
counter_max = _to_number(raw.get("max"))
|
|
242
|
+
counter_value = _to_number(raw.get("value"))
|
|
243
|
+
|
|
244
|
+
visibility = raw.get("visibility")
|
|
245
|
+
if visibility is not None and not isinstance(visibility, dict):
|
|
246
|
+
visibility = None
|
|
247
|
+
|
|
248
|
+
# Component-specific config
|
|
249
|
+
lookup_object = raw.get("lookupObject")
|
|
250
|
+
lookup_object = str(lookup_object).strip() if lookup_object else None
|
|
251
|
+
lookup_search_fields = raw.get("lookupSearchFields")
|
|
252
|
+
lookup_search_fields = str(lookup_search_fields).strip() if lookup_search_fields else None
|
|
253
|
+
lookup_multi = bool(raw.get("lookupMulti")) or bool(raw.get("isMultiSelection"))
|
|
254
|
+
file_name = raw.get("fileName")
|
|
255
|
+
file_name = str(file_name).strip() if file_name else None
|
|
256
|
+
|
|
257
|
+
# Matrix uses "matrixRows" as per-row prompt text. The dcMatrix component
|
|
258
|
+
# exposes those as the "questions" input parameter. options become the
|
|
259
|
+
# column choices (choiceReferences).
|
|
260
|
+
matrix_questions: list[str] | None = None
|
|
261
|
+
if field_type == "Matrix":
|
|
262
|
+
matrix_questions = matrix_rows
|
|
263
|
+
|
|
264
|
+
return Field(
|
|
265
|
+
name=api_name,
|
|
266
|
+
label=label,
|
|
267
|
+
field_type=field_type,
|
|
268
|
+
is_required=is_required,
|
|
269
|
+
default_value=default_value,
|
|
270
|
+
options=options,
|
|
271
|
+
matrix_rows=matrix_rows,
|
|
272
|
+
matrix_questions=matrix_questions,
|
|
273
|
+
repeater_fields=repeater_children,
|
|
274
|
+
counter_min=counter_min,
|
|
275
|
+
counter_max=counter_max,
|
|
276
|
+
counter_value=counter_value,
|
|
277
|
+
visibility=visibility,
|
|
278
|
+
lookup_object=lookup_object,
|
|
279
|
+
lookup_search_fields=lookup_search_fields,
|
|
280
|
+
lookup_multi=lookup_multi,
|
|
281
|
+
file_name=file_name,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _to_number(v: Any) -> float | None:
|
|
286
|
+
if v is None or isinstance(v, bool):
|
|
287
|
+
return None
|
|
288
|
+
if isinstance(v, (int, float)):
|
|
289
|
+
return float(v)
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def uniquify(name: str, used: set[str]) -> str:
|
|
294
|
+
if name not in used:
|
|
295
|
+
used.add(name)
|
|
296
|
+
return name
|
|
297
|
+
i = 2
|
|
298
|
+
while f"{name}_{i}" in used:
|
|
299
|
+
i += 1
|
|
300
|
+
final = f"{name}_{i}"
|
|
301
|
+
used.add(final)
|
|
302
|
+
return final
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def parse_spec(data: dict) -> FormSpec:
|
|
306
|
+
title = (data.get("formTitle") or "Data Capture Form").strip()
|
|
307
|
+
form_type = (data.get("formType") or "").strip()
|
|
308
|
+
screens_raw = data.get("screens")
|
|
309
|
+
if not isinstance(screens_raw, dict) or not screens_raw:
|
|
310
|
+
raise ValueError("spec must have a non-empty 'screens' object")
|
|
311
|
+
|
|
312
|
+
used_names: set[str] = set()
|
|
313
|
+
spec = FormSpec(title=title, form_type=form_type, screens=[])
|
|
314
|
+
for screen_key, fields_raw in screens_raw.items():
|
|
315
|
+
if not isinstance(fields_raw, list) or not fields_raw:
|
|
316
|
+
raise ValueError(f"screen '{screen_key}' must have a non-empty list of fields")
|
|
317
|
+
screen_api = uniquify(coerce_api_name(f"Screen_{screen_key}"), used_names)
|
|
318
|
+
screen_label = humanize_screen_key(screen_key)
|
|
319
|
+
|
|
320
|
+
# Split items into (Section header dict | field) sequence. A header is
|
|
321
|
+
# any dict with a "section" key and no "fieldName".
|
|
322
|
+
sections: list[Section] = []
|
|
323
|
+
flat_fields: list[Field] = []
|
|
324
|
+
current_section_label: str | None = None
|
|
325
|
+
current_section_fields: list[Field] = []
|
|
326
|
+
# FIX I1: section_names_used now seeds from used_names to prevent
|
|
327
|
+
# flow-global collisions (e.g. Sec_General on multiple screens).
|
|
328
|
+
section_names_used: set[str] = used_names.copy()
|
|
329
|
+
|
|
330
|
+
def flush_section():
|
|
331
|
+
if current_section_label is None:
|
|
332
|
+
return
|
|
333
|
+
api = uniquify(
|
|
334
|
+
coerce_api_name(f"Sec_{current_section_label}"),
|
|
335
|
+
section_names_used,
|
|
336
|
+
)
|
|
337
|
+
# FIX I1: register section name in global used_names to prevent
|
|
338
|
+
# cross-screen collisions (e.g. multiple "Sec_General" sections).
|
|
339
|
+
used_names.add(api)
|
|
340
|
+
sections.append(Section(
|
|
341
|
+
name=api,
|
|
342
|
+
label=current_section_label,
|
|
343
|
+
fields=current_section_fields[:],
|
|
344
|
+
))
|
|
345
|
+
|
|
346
|
+
def build_nested_section(item: dict, used: set[str],
|
|
347
|
+
sec_names: set[str], depth: int) -> Section:
|
|
348
|
+
label = str(item.get("section") or "Section").strip() or "Section"
|
|
349
|
+
api = uniquify(coerce_api_name(f"Sec_{label}"), sec_names)
|
|
350
|
+
# FIX I1: register nested section name in global used_names.
|
|
351
|
+
used.add(api)
|
|
352
|
+
children: list = []
|
|
353
|
+
for child in item.get("fields") or []:
|
|
354
|
+
if isinstance(child, dict) and "section" in child and "fields" in child:
|
|
355
|
+
if depth + 1 >= MAX_SECTION_DEPTH:
|
|
356
|
+
# Over-cap: nesting stops at MAX_SECTION_DEPTH. Flatten ALL
|
|
357
|
+
# descendant leaf fields of this over-cap subtree into the
|
|
358
|
+
# current section (no field is dropped). NOTE: the over-cap
|
|
359
|
+
# section's own visibilityRule is intentionally not preserved
|
|
360
|
+
# here — the converter's depth-cap flagging (WI-7) surfaces
|
|
361
|
+
# over-cap chains to the user, honoring "flag, never silently
|
|
362
|
+
# drop".
|
|
363
|
+
def _flatten_leaves(node_fields):
|
|
364
|
+
for leaf in node_fields:
|
|
365
|
+
if isinstance(leaf, dict) and "fieldName" in leaf:
|
|
366
|
+
yield parse_field(leaf, used, spec.warnings)
|
|
367
|
+
elif isinstance(leaf, dict) and "fields" in leaf:
|
|
368
|
+
yield from _flatten_leaves(leaf.get("fields") or [])
|
|
369
|
+
children.extend(_flatten_leaves(child.get("fields") or []))
|
|
370
|
+
continue
|
|
371
|
+
children.append(build_nested_section(child, used, sec_names, depth + 1))
|
|
372
|
+
elif isinstance(child, dict) and "fieldName" in child:
|
|
373
|
+
children.append(parse_field(child, used, spec.warnings))
|
|
374
|
+
sec = Section(name=api, label=label, fields=children)
|
|
375
|
+
vis = item.get("visibility")
|
|
376
|
+
sec.visibility = vis if isinstance(vis, dict) else None
|
|
377
|
+
return sec
|
|
378
|
+
|
|
379
|
+
has_sections = any(
|
|
380
|
+
isinstance(item, dict) and "section" in item and "fieldName" not in item
|
|
381
|
+
for item in fields_raw
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
for item in fields_raw:
|
|
385
|
+
if has_sections and isinstance(item, dict) and "section" in item and "fieldName" not in item:
|
|
386
|
+
if "fields" in item:
|
|
387
|
+
# Self-contained (possibly nested) section.
|
|
388
|
+
flush_section()
|
|
389
|
+
current_section_label = None
|
|
390
|
+
sections.append(build_nested_section(
|
|
391
|
+
item, used_names, section_names_used, depth=0))
|
|
392
|
+
continue
|
|
393
|
+
# Legacy inline opener (unchanged).
|
|
394
|
+
flush_section()
|
|
395
|
+
current_section_label = str(item["section"]).strip() or "Section"
|
|
396
|
+
current_section_fields = []
|
|
397
|
+
continue
|
|
398
|
+
f = parse_field(item, used_names, spec.warnings)
|
|
399
|
+
if has_sections:
|
|
400
|
+
if current_section_label is None:
|
|
401
|
+
# Fields before the first sentinel: bucket into implicit "General".
|
|
402
|
+
current_section_label = "General"
|
|
403
|
+
current_section_fields = []
|
|
404
|
+
current_section_fields.append(f)
|
|
405
|
+
else:
|
|
406
|
+
flat_fields.append(f)
|
|
407
|
+
|
|
408
|
+
if has_sections:
|
|
409
|
+
flush_section()
|
|
410
|
+
spec.screens.append(Screen(
|
|
411
|
+
name=screen_api,
|
|
412
|
+
label=screen_label,
|
|
413
|
+
fields=list(_walk_fields([ff for s in sections for ff in s.fields])),
|
|
414
|
+
sections=sections,
|
|
415
|
+
))
|
|
416
|
+
else:
|
|
417
|
+
spec.screens.append(Screen(name=screen_api, label=screen_label, fields=flat_fields))
|
|
418
|
+
|
|
419
|
+
# Hoist choices for choice/multichoice/matrix strategies (top-level + repeater children).
|
|
420
|
+
# Matrix uses choiceReferences for its column options exactly like Picklist.
|
|
421
|
+
screen_obj = spec.screens[-1]
|
|
422
|
+
for f in _walk_fields(screen_obj.fields):
|
|
423
|
+
strategy = TYPE_MAP[f.field_type][1]
|
|
424
|
+
if strategy in ("choice", "multichoice", "matrix") and f.options:
|
|
425
|
+
for opt in f.options:
|
|
426
|
+
cname = uniquify_choice(opt, spec.choices)
|
|
427
|
+
spec.choices.setdefault(cname, opt)
|
|
428
|
+
spec.fields_by_name[f.name] = f
|
|
429
|
+
|
|
430
|
+
spec.post_screen = data.get("postScreen") if isinstance(data.get("postScreen"), dict) else None
|
|
431
|
+
raw_decisions = data.get("decisions")
|
|
432
|
+
spec.decisions = raw_decisions if isinstance(raw_decisions, list) else []
|
|
433
|
+
raw_conns = data.get("_screenConnectors")
|
|
434
|
+
spec.screen_connectors = raw_conns if isinstance(raw_conns, dict) else {}
|
|
435
|
+
inc = data.get("_incompleteMigration")
|
|
436
|
+
if isinstance(inc, list):
|
|
437
|
+
spec.incomplete_migration = inc
|
|
438
|
+
return spec
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _walk_fields(fields: list):
|
|
442
|
+
for f in fields:
|
|
443
|
+
if isinstance(f, Section):
|
|
444
|
+
yield from _walk_fields(f.fields)
|
|
445
|
+
continue
|
|
446
|
+
yield f
|
|
447
|
+
if f.repeater_fields:
|
|
448
|
+
yield from _walk_fields(f.repeater_fields)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def uniquify_choice(display: str, existing: dict[str, str]) -> str:
|
|
452
|
+
"""Choice api_name = sanitized display, but if same display already mapped, reuse it."""
|
|
453
|
+
for k, v in existing.items():
|
|
454
|
+
if v == display:
|
|
455
|
+
return k
|
|
456
|
+
base = coerce_api_name(display)
|
|
457
|
+
if base in existing:
|
|
458
|
+
i = 2
|
|
459
|
+
while f"{base}_{i}" in existing:
|
|
460
|
+
i += 1
|
|
461
|
+
return f"{base}_{i}"
|
|
462
|
+
return base
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ---- XML emission --------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def x(text: str) -> str:
|
|
469
|
+
return xml_escape(text or "", {'"': """, "'": "'"})
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def render_input_param(name: str, value: str | None = None, *,
|
|
473
|
+
boolean_value: bool | None = None,
|
|
474
|
+
number_value: float | None = None,
|
|
475
|
+
ref_value: str | None = None,
|
|
476
|
+
indent: str = " ") -> str:
|
|
477
|
+
parts = [f"{indent}<inputParameters>\n",
|
|
478
|
+
f"{indent} <name>{x(name)}</name>\n",
|
|
479
|
+
f"{indent} <value>\n"]
|
|
480
|
+
if ref_value is not None:
|
|
481
|
+
parts.append(f"{indent} <elementReference>{x(ref_value)}</elementReference>\n")
|
|
482
|
+
elif value is not None:
|
|
483
|
+
parts.append(f"{indent} <stringValue>{x(value)}</stringValue>\n")
|
|
484
|
+
elif boolean_value is not None:
|
|
485
|
+
parts.append(f"{indent} <booleanValue>{'true' if boolean_value else 'false'}</booleanValue>\n")
|
|
486
|
+
elif number_value is not None:
|
|
487
|
+
parts.append(f"{indent} <numberValue>{_fmt_num(number_value)}</numberValue>\n")
|
|
488
|
+
parts.extend([f"{indent} </value>\n", f"{indent}</inputParameters>\n"])
|
|
489
|
+
return "".join(parts)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _fmt_num(n: float) -> str:
|
|
493
|
+
# FIX I5: Use fixed/decimal formatting to avoid scientific notation (repr(1e-5)=='1e-05').
|
|
494
|
+
# Salesforce expects e.g. "1.0" not "1" for integers.
|
|
495
|
+
if n == int(n):
|
|
496
|
+
return f"{n:.1f}"
|
|
497
|
+
# Use .15g for compact representation, then convert any scientific notation to fixed.
|
|
498
|
+
# This balances precision (avoiding floating-point artifacts like 123.456000000000003)
|
|
499
|
+
# with readability (avoiding 1e-05).
|
|
500
|
+
formatted = f"{n:.15g}"
|
|
501
|
+
# If .15g produced scientific notation, convert to fixed-point.
|
|
502
|
+
if 'e' in formatted or 'E' in formatted:
|
|
503
|
+
# Use sufficient decimal places to represent the number accurately.
|
|
504
|
+
formatted = f"{n:.15f}".rstrip('0').rstrip('.')
|
|
505
|
+
# Ensure there's a decimal point for Salesforce (e.g. "5" -> "5.0").
|
|
506
|
+
if '.' not in formatted:
|
|
507
|
+
formatted += ".0"
|
|
508
|
+
return formatted
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def render_style_properties(indent: str) -> str:
|
|
512
|
+
return (
|
|
513
|
+
f"{indent}<styleProperties>\n"
|
|
514
|
+
f"{indent} <verticalAlignment>\n"
|
|
515
|
+
f"{indent} <stringValue>top</stringValue>\n"
|
|
516
|
+
f"{indent} </verticalAlignment>\n"
|
|
517
|
+
f"{indent} <width>\n"
|
|
518
|
+
f"{indent} <stringValue>12</stringValue>\n"
|
|
519
|
+
f"{indent} </width>\n"
|
|
520
|
+
f"{indent}</styleProperties>\n"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def find_choice_name(spec: FormSpec, display: str) -> str | None:
|
|
525
|
+
for k, v in spec.choices.items():
|
|
526
|
+
if v == display:
|
|
527
|
+
return k
|
|
528
|
+
return None
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def render_visibility_rule(field: Field, spec: FormSpec, indent: str) -> str:
|
|
532
|
+
"""Emit a <visibilityRule> for the field's visibility spec.
|
|
533
|
+
|
|
534
|
+
Two accepted shapes, discriminated by the "conditions" key:
|
|
535
|
+
(a) legacy {field, operator, value} — resolves value to a choice
|
|
536
|
+
api-name, rightValue booleanValue=true, conditionLogic=and.
|
|
537
|
+
(b) multi {conditionLogic, conditions:[{leftValueReference, operator,
|
|
538
|
+
rightValue:{<type>:v}}]} — leftValueReference used verbatim; rendered
|
|
539
|
+
via render_conditions (18 operators, all typed rightValues).
|
|
540
|
+
"""
|
|
541
|
+
v = field.visibility or {}
|
|
542
|
+
|
|
543
|
+
# Shape (b): multi-condition. leftValueReference is already a resolved
|
|
544
|
+
# element/field name from the converter — used verbatim.
|
|
545
|
+
if "conditions" in v:
|
|
546
|
+
conditions = v.get("conditions") or []
|
|
547
|
+
if not conditions:
|
|
548
|
+
spec.warnings.append(
|
|
549
|
+
f"visibility on {field.name}: empty conditions, skipped")
|
|
550
|
+
return ""
|
|
551
|
+
for cond in conditions:
|
|
552
|
+
left = (cond.get("leftValueReference") or "").split(".")[0]
|
|
553
|
+
if not left or left not in spec.fields_by_name:
|
|
554
|
+
spec.warnings.append(
|
|
555
|
+
f"visibility on {field.name}: leftValueReference "
|
|
556
|
+
f"'{cond.get('leftValueReference')}' not found, skipped")
|
|
557
|
+
return ""
|
|
558
|
+
inner = indent + " "
|
|
559
|
+
return (f"{indent}<visibilityRule>\n"
|
|
560
|
+
+ render_conditions(conditions, v.get("conditionLogic") or "and", inner)
|
|
561
|
+
+ f"{indent}</visibilityRule>\n")
|
|
562
|
+
|
|
563
|
+
# Shape (a): legacy choice-boolean rule (unchanged).
|
|
564
|
+
target_field = (v.get("field") or "").strip()
|
|
565
|
+
target_value = (v.get("value") or "").strip()
|
|
566
|
+
operator = (v.get("operator") or "EqualTo").strip()
|
|
567
|
+
if not target_field or not target_value:
|
|
568
|
+
spec.warnings.append(
|
|
569
|
+
f"visibility on {field.name}: missing field/value, skipped")
|
|
570
|
+
return ""
|
|
571
|
+
parent = spec.fields_by_name.get(target_field)
|
|
572
|
+
if parent is None:
|
|
573
|
+
spec.warnings.append(
|
|
574
|
+
f"visibility on {field.name}: field '{target_field}' not found, skipped")
|
|
575
|
+
return ""
|
|
576
|
+
if parent.field_type not in ("Picklist", "Radio", "CheckboxGroup"):
|
|
577
|
+
spec.warnings.append(
|
|
578
|
+
f"visibility on {field.name}: field '{target_field}' is not a choice field, skipped")
|
|
579
|
+
return ""
|
|
580
|
+
choice_name = find_choice_name(spec, target_value)
|
|
581
|
+
if choice_name is None:
|
|
582
|
+
spec.warnings.append(
|
|
583
|
+
f"visibility on {field.name}: value '{target_value}' is not one of "
|
|
584
|
+
f"'{target_field}'s options, skipped")
|
|
585
|
+
return ""
|
|
586
|
+
|
|
587
|
+
# FIX I6: legacy shape (a) always emits rightValue booleanValue=true, so warn
|
|
588
|
+
# if the operator is not EqualTo (e.g. "NotEqualTo ... true" is likely unintended).
|
|
589
|
+
if operator != "EqualTo":
|
|
590
|
+
spec.warnings.append(
|
|
591
|
+
f"visibility on {field.name}: legacy shape (a) always emits 'rightValue booleanValue=true'; "
|
|
592
|
+
f"operator '{operator}' may produce unexpected results (consider shape (b) for complex logic)")
|
|
593
|
+
|
|
594
|
+
inner = indent + " "
|
|
595
|
+
cinner = inner + " "
|
|
596
|
+
rinner = cinner + " "
|
|
597
|
+
parts = [
|
|
598
|
+
f"{indent}<visibilityRule>\n",
|
|
599
|
+
f"{inner}<conditionLogic>and</conditionLogic>\n",
|
|
600
|
+
f"{inner}<conditions>\n",
|
|
601
|
+
f"{cinner}<leftValueReference>{x(choice_name)}</leftValueReference>\n",
|
|
602
|
+
f"{cinner}<operator>{x(operator)}</operator>\n",
|
|
603
|
+
f"{cinner}<rightValue>\n",
|
|
604
|
+
f"{rinner}<booleanValue>true</booleanValue>\n",
|
|
605
|
+
f"{cinner}</rightValue>\n",
|
|
606
|
+
f"{inner}</conditions>\n",
|
|
607
|
+
f"{indent}</visibilityRule>\n",
|
|
608
|
+
]
|
|
609
|
+
return "".join(parts)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def render_field(field: Field, spec: FormSpec, indent: str = " ") -> str:
|
|
613
|
+
extension_name, strategy, fallback_prefix, _status, extra_inputs = TYPE_MAP[field.field_type]
|
|
614
|
+
|
|
615
|
+
# Lookup needs `objectApiName`; without it, fall back to a labeled ShortText
|
|
616
|
+
# so the flow still deploys and the admin can wire it up.
|
|
617
|
+
if field.field_type == "Lookup" and not field.lookup_object:
|
|
618
|
+
spec.warnings.append(
|
|
619
|
+
f"Lookup '{field.name}': no `lookupObject` provided; rendering as ShortText placeholder."
|
|
620
|
+
)
|
|
621
|
+
extension_name = "runtime_service_fieldservice:dcTextInput"
|
|
622
|
+
strategy = "instance"
|
|
623
|
+
fallback_prefix = "[Lookup — set objectApiName in Flow Builder] "
|
|
624
|
+
extra_inputs = []
|
|
625
|
+
|
|
626
|
+
# FileView needs `fileName`; without it, fall back to a labeled ShortText placeholder.
|
|
627
|
+
if field.field_type == "FileView" and not field.file_name:
|
|
628
|
+
spec.warnings.append(
|
|
629
|
+
f"FileView '{field.name}': no `fileName` provided; rendering as ShortText placeholder."
|
|
630
|
+
)
|
|
631
|
+
extension_name = "runtime_service_fieldservice:dcTextInput"
|
|
632
|
+
strategy = "instance"
|
|
633
|
+
fallback_prefix = "[FileView — set fileName in Flow Builder] "
|
|
634
|
+
extra_inputs = []
|
|
635
|
+
|
|
636
|
+
label = field.label
|
|
637
|
+
if fallback_prefix:
|
|
638
|
+
label = f"{fallback_prefix}{label}"
|
|
639
|
+
|
|
640
|
+
parts: list[str] = []
|
|
641
|
+
parts.append(f"{indent}<fields>\n")
|
|
642
|
+
parts.append(f"{indent} <name>{x(field.name)}</name>\n")
|
|
643
|
+
|
|
644
|
+
inner = indent + " "
|
|
645
|
+
|
|
646
|
+
if strategy == "displaytext":
|
|
647
|
+
body = field.default_value if isinstance(field.default_value, str) and field.default_value else label
|
|
648
|
+
parts.append(f"{indent} <fieldText>{x('<p>' + body + '</p>')}</fieldText>\n")
|
|
649
|
+
parts.append(f"{indent} <fieldType>DisplayText</fieldType>\n")
|
|
650
|
+
parts.append(render_style_properties(inner))
|
|
651
|
+
parts.append(f"{indent}</fields>\n")
|
|
652
|
+
return "".join(parts)
|
|
653
|
+
|
|
654
|
+
if strategy == "repeater":
|
|
655
|
+
parts.append(f"{indent} <fieldType>Repeater</fieldType>\n")
|
|
656
|
+
for child in field.repeater_fields or []:
|
|
657
|
+
parts.append(render_field(child, spec, indent + " "))
|
|
658
|
+
parts.append(f"{indent} <isRequired>{'true' if field.is_required else 'false'}</isRequired>\n")
|
|
659
|
+
parts.append(render_style_properties(inner))
|
|
660
|
+
if field.visibility:
|
|
661
|
+
parts.append(render_visibility_rule(field, spec, inner))
|
|
662
|
+
parts.append(f"{indent}</fields>\n")
|
|
663
|
+
return "".join(parts)
|
|
664
|
+
|
|
665
|
+
if strategy in ("choice", "multichoice"):
|
|
666
|
+
for opt in field.options or []:
|
|
667
|
+
cname = find_choice_name(spec, opt)
|
|
668
|
+
if cname:
|
|
669
|
+
parts.append(f"{indent} <choiceReferences>{x(cname)}</choiceReferences>\n")
|
|
670
|
+
if field.default_value and isinstance(field.default_value, str):
|
|
671
|
+
cname = find_choice_name(spec, field.default_value)
|
|
672
|
+
if cname:
|
|
673
|
+
parts.append(f"{indent} <defaultValue>\n")
|
|
674
|
+
parts.append(f"{indent} <elementReference>{x(cname)}</elementReference>\n")
|
|
675
|
+
parts.append(f"{indent} </defaultValue>\n")
|
|
676
|
+
parts.append(f"{indent} <extensionName>{x(extension_name)}</extensionName>\n")
|
|
677
|
+
parts.append(f"{indent} <fieldText>{x(label)}</fieldText>\n")
|
|
678
|
+
parts.append(f"{indent} <fieldType>{'ComponentChoice' if strategy == 'choice' else 'ComponentMultiChoice'}</fieldType>\n")
|
|
679
|
+
parts.append(f"{indent} <inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>\n")
|
|
680
|
+
parts.append(f"{indent} <isRequired>{'true' if field.is_required else 'false'}</isRequired>\n")
|
|
681
|
+
parts.append(f"{indent} <storeOutputAutomatically>true</storeOutputAutomatically>\n")
|
|
682
|
+
parts.append(render_style_properties(inner))
|
|
683
|
+
if field.visibility:
|
|
684
|
+
parts.append(render_visibility_rule(field, spec, inner))
|
|
685
|
+
parts.append(f"{indent}</fields>\n")
|
|
686
|
+
return "".join(parts)
|
|
687
|
+
|
|
688
|
+
if strategy == "matrix":
|
|
689
|
+
# dcMatrix is a ComponentMultiChoice that takes column options via
|
|
690
|
+
# <choiceReferences> and row labels via the "questions" input parameter
|
|
691
|
+
# (a JSON-encoded array of strings). Verified May 2026 from
|
|
692
|
+
# Data_Capture_All_Components.flow-meta.xml.
|
|
693
|
+
for opt in field.options or []:
|
|
694
|
+
cname = find_choice_name(spec, opt)
|
|
695
|
+
if cname:
|
|
696
|
+
parts.append(f"{indent} <choiceReferences>{x(cname)}</choiceReferences>\n")
|
|
697
|
+
if field.default_value and isinstance(field.default_value, str):
|
|
698
|
+
cname = find_choice_name(spec, field.default_value)
|
|
699
|
+
if cname:
|
|
700
|
+
parts.append(f"{indent} <defaultValue>\n")
|
|
701
|
+
parts.append(f"{indent} <elementReference>{x(cname)}</elementReference>\n")
|
|
702
|
+
parts.append(f"{indent} </defaultValue>\n")
|
|
703
|
+
parts.append(f"{indent} <extensionName>{x(extension_name)}</extensionName>\n")
|
|
704
|
+
parts.append(f"{indent} <fieldText>{x(label)}</fieldText>\n")
|
|
705
|
+
parts.append(f"{indent} <fieldType>ComponentMultiChoice</fieldType>\n")
|
|
706
|
+
if field.matrix_questions:
|
|
707
|
+
questions_json = json.dumps(field.matrix_questions, ensure_ascii=False)
|
|
708
|
+
parts.append(render_input_param("questions", questions_json, indent=inner))
|
|
709
|
+
parts.append(f"{indent} <inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>\n")
|
|
710
|
+
parts.append(f"{indent} <isRequired>{'true' if field.is_required else 'false'}</isRequired>\n")
|
|
711
|
+
parts.append(f"{indent} <storeOutputAutomatically>true</storeOutputAutomatically>\n")
|
|
712
|
+
parts.append(render_style_properties(inner))
|
|
713
|
+
if field.visibility:
|
|
714
|
+
parts.append(render_visibility_rule(field, spec, inner))
|
|
715
|
+
parts.append(f"{indent}</fields>\n")
|
|
716
|
+
return "".join(parts)
|
|
717
|
+
|
|
718
|
+
# strategy == "instance"
|
|
719
|
+
parts.append(f"{indent} <extensionName>{x(extension_name)}</extensionName>\n")
|
|
720
|
+
parts.append(f"{indent} <fieldType>ComponentInstance</fieldType>\n")
|
|
721
|
+
parts.append(render_input_param("label", label, indent=inner))
|
|
722
|
+
if field.is_required:
|
|
723
|
+
parts.append(render_input_param("required", boolean_value=True, indent=inner))
|
|
724
|
+
if field.field_type == "Counter":
|
|
725
|
+
if field.counter_min is not None:
|
|
726
|
+
parts.append(render_input_param("min", number_value=field.counter_min, indent=inner))
|
|
727
|
+
if field.counter_max is not None:
|
|
728
|
+
parts.append(render_input_param("max", number_value=field.counter_max, indent=inner))
|
|
729
|
+
if field.counter_value is not None:
|
|
730
|
+
parts.append(render_input_param("value", number_value=field.counter_value, indent=inner))
|
|
731
|
+
# Component-specific input parameters
|
|
732
|
+
if field.field_type == "Lookup" and field.lookup_object:
|
|
733
|
+
parts.append(render_input_param("objectApiName", field.lookup_object, indent=inner))
|
|
734
|
+
if field.lookup_search_fields:
|
|
735
|
+
parts.append(render_input_param("searchedFields", field.lookup_search_fields, indent=inner))
|
|
736
|
+
if field.lookup_multi:
|
|
737
|
+
parts.append(render_input_param("isMultiSelection", boolean_value=True, indent=inner))
|
|
738
|
+
if field.field_type == "FileView" and field.file_name:
|
|
739
|
+
parts.append(render_input_param("fileName", field.file_name, indent=inner))
|
|
740
|
+
# Auto-injected component plumbing (e.g. UploadImage's recordId, Signature's
|
|
741
|
+
# parentRecordId). These reference the standard DataCaptureFlow input vars.
|
|
742
|
+
for param_name, kind, value in extra_inputs:
|
|
743
|
+
if kind == "ref":
|
|
744
|
+
parts.append(render_input_param(param_name, ref_value=value, indent=inner))
|
|
745
|
+
elif kind == "bool":
|
|
746
|
+
parts.append(render_input_param(param_name, boolean_value=(value == "true"), indent=inner))
|
|
747
|
+
else:
|
|
748
|
+
parts.append(render_input_param(param_name, value, indent=inner))
|
|
749
|
+
if field.default_value is not None and isinstance(field.default_value, (str, int, float, bool)) \
|
|
750
|
+
and field.field_type != "Counter":
|
|
751
|
+
if isinstance(field.default_value, bool):
|
|
752
|
+
parts.append(render_input_param("defaultValue", boolean_value=field.default_value, indent=inner))
|
|
753
|
+
elif isinstance(field.default_value, (int, float)):
|
|
754
|
+
parts.append(render_input_param("defaultValue", number_value=float(field.default_value), indent=inner))
|
|
755
|
+
else:
|
|
756
|
+
parts.append(render_input_param("defaultValue", str(field.default_value), indent=inner))
|
|
757
|
+
parts.append(f"{indent} <inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>\n")
|
|
758
|
+
parts.append(f"{indent} <isRequired>{'true' if field.is_required else 'false'}</isRequired>\n")
|
|
759
|
+
parts.append(f"{indent} <storeOutputAutomatically>true</storeOutputAutomatically>\n")
|
|
760
|
+
parts.append(render_style_properties(inner))
|
|
761
|
+
if field.visibility:
|
|
762
|
+
parts.append(render_visibility_rule(field, spec, inner))
|
|
763
|
+
parts.append(f"{indent}</fields>\n")
|
|
764
|
+
return "".join(parts)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def render_section(section: Section, spec: FormSpec, indent: str = " ") -> str:
|
|
768
|
+
"""Render a Salesforce Flow Section component.
|
|
769
|
+
|
|
770
|
+
Encoded as a single <fields> block of fieldType=RegionContainer with one
|
|
771
|
+
inner <fields> block of fieldType=Region (single column, full width 12).
|
|
772
|
+
Mirrors the verified pattern in
|
|
773
|
+
.claude/skills/salesforce-data-capture/examples/Data_Capture_All_Components.flow-meta.xml.
|
|
774
|
+
"""
|
|
775
|
+
inner = indent + " "
|
|
776
|
+
region_inner = inner + " "
|
|
777
|
+
parts: list[str] = []
|
|
778
|
+
parts.append(f"{indent}<fields>\n")
|
|
779
|
+
parts.append(f"{inner}<name>{x(section.name)}</name>\n")
|
|
780
|
+
parts.append(f"{inner}<fieldText>{x(section.label)}</fieldText>\n")
|
|
781
|
+
parts.append(f"{inner}<fieldType>RegionContainer</fieldType>\n")
|
|
782
|
+
# Single Region child (one column, width 12).
|
|
783
|
+
parts.append(f"{inner}<fields>\n")
|
|
784
|
+
parts.append(f"{region_inner}<name>{x(section.name)}_Col1</name>\n")
|
|
785
|
+
parts.append(f"{region_inner}<fieldType>Region</fieldType>\n")
|
|
786
|
+
for field in section.fields:
|
|
787
|
+
if isinstance(field, Section):
|
|
788
|
+
parts.append(render_section(field, spec, indent=region_inner))
|
|
789
|
+
else:
|
|
790
|
+
parts.append(render_field(field, spec, indent=region_inner))
|
|
791
|
+
parts.append(f"{region_inner}<inputParameters>\n")
|
|
792
|
+
parts.append(f"{region_inner} <name>width</name>\n")
|
|
793
|
+
parts.append(f"{region_inner} <value>\n")
|
|
794
|
+
parts.append(f"{region_inner} <stringValue>12</stringValue>\n")
|
|
795
|
+
parts.append(f"{region_inner} </value>\n")
|
|
796
|
+
parts.append(f"{region_inner}</inputParameters>\n")
|
|
797
|
+
parts.append(f"{region_inner}<isRequired>false</isRequired>\n")
|
|
798
|
+
parts.append(f"{inner}</fields>\n")
|
|
799
|
+
parts.append(f"{inner}<isRequired>false</isRequired>\n")
|
|
800
|
+
parts.append(f"{inner}<regionContainerType>SectionWithHeader</regionContainerType>\n")
|
|
801
|
+
parts.append(render_style_properties(inner))
|
|
802
|
+
if section.visibility and section.visibility.get("conditions"):
|
|
803
|
+
parts.append(
|
|
804
|
+
f"{inner}<visibilityRule>\n"
|
|
805
|
+
+ render_conditions(section.visibility["conditions"],
|
|
806
|
+
section.visibility.get("conditionLogic") or "and",
|
|
807
|
+
inner + " ")
|
|
808
|
+
+ f"{inner}</visibilityRule>\n"
|
|
809
|
+
)
|
|
810
|
+
parts.append(f"{indent}</fields>\n")
|
|
811
|
+
return "".join(parts)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def render_screen(screen: Screen, spec: FormSpec, *, next_target: str | None) -> str:
|
|
815
|
+
parts: list[str] = []
|
|
816
|
+
parts.append(" <screens>\n")
|
|
817
|
+
parts.append(f" <name>{x(screen.name)}</name>\n")
|
|
818
|
+
parts.append(f" <label>{x(screen.label)}</label>\n")
|
|
819
|
+
parts.append(" <locationX>0</locationX>\n")
|
|
820
|
+
parts.append(" <locationY>0</locationY>\n")
|
|
821
|
+
parts.append(" <allowBack>true</allowBack>\n")
|
|
822
|
+
parts.append(" <allowFinish>true</allowFinish>\n")
|
|
823
|
+
parts.append(" <allowPause>true</allowPause>\n")
|
|
824
|
+
if next_target:
|
|
825
|
+
parts.append(" <connector>\n")
|
|
826
|
+
parts.append(f" <targetReference>{x(next_target)}</targetReference>\n")
|
|
827
|
+
parts.append(" </connector>\n")
|
|
828
|
+
if screen.sections:
|
|
829
|
+
for section in screen.sections:
|
|
830
|
+
parts.append(render_section(section, spec))
|
|
831
|
+
else:
|
|
832
|
+
for field in screen.fields:
|
|
833
|
+
parts.append(render_field(field, spec))
|
|
834
|
+
parts.append(" <showFooter>true</showFooter>\n")
|
|
835
|
+
parts.append(" <showHeader>true</showHeader>\n")
|
|
836
|
+
parts.append(" </screens>\n")
|
|
837
|
+
return "".join(parts)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def render_choice(name: str, display: str) -> str:
|
|
841
|
+
return (
|
|
842
|
+
" <choices>\n"
|
|
843
|
+
f" <name>{x(name)}</name>\n"
|
|
844
|
+
f" <choiceText>{x(display)}</choiceText>\n"
|
|
845
|
+
" <dataType>String</dataType>\n"
|
|
846
|
+
" <value>\n"
|
|
847
|
+
f" <stringValue>{x(display)}</stringValue>\n"
|
|
848
|
+
" </value>\n"
|
|
849
|
+
" </choices>\n"
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
# ---- post-screen automation ----------------------------------------------
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _render_value(spec: FormSpec, raw: dict | None,
|
|
857
|
+
*, value_key: str = "value", ref_key: str = "valueRef",
|
|
858
|
+
bool_key: str = "boolean", num_key: str = "number",
|
|
859
|
+
indent: str = " ") -> str:
|
|
860
|
+
"""Render a <value>...</value> block from a raw spec dict.
|
|
861
|
+
|
|
862
|
+
Recognised keys: valueRef (elementReference), value (stringValue),
|
|
863
|
+
boolean (booleanValue), number (numberValue).
|
|
864
|
+
"""
|
|
865
|
+
raw = raw or {}
|
|
866
|
+
parts = [f"{indent}<value>\n"]
|
|
867
|
+
inner = indent + " "
|
|
868
|
+
if ref_key in raw and raw[ref_key] is not None:
|
|
869
|
+
parts.append(f"{inner}<elementReference>{x(str(raw[ref_key]))}</elementReference>\n")
|
|
870
|
+
elif bool_key in raw and raw[bool_key] is not None:
|
|
871
|
+
parts.append(f"{inner}<booleanValue>{'true' if raw[bool_key] else 'false'}</booleanValue>\n")
|
|
872
|
+
elif num_key in raw and raw[num_key] is not None:
|
|
873
|
+
parts.append(f"{inner}<numberValue>{_fmt_num(float(raw[num_key]))}</numberValue>\n")
|
|
874
|
+
elif value_key in raw and raw[value_key] is not None:
|
|
875
|
+
parts.append(f"{inner}<stringValue>{x(str(raw[value_key]))}</stringValue>\n")
|
|
876
|
+
parts.append(f"{indent}</value>\n")
|
|
877
|
+
return "".join(parts)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def render_conditions(conditions: list[dict], condition_logic: str, indent: str) -> str:
|
|
881
|
+
"""Render a <conditionLogic> line + one <conditions> block per condition.
|
|
882
|
+
|
|
883
|
+
`indent` is the indent of the <conditionLogic>/<conditions> tags; condition
|
|
884
|
+
children (leftValueReference/operator/rightValue) sit at indent + 4 spaces.
|
|
885
|
+
rightValue typing is delegated to _render_decision_right_value, so all 18
|
|
886
|
+
operators and every typed value render through a single code path.
|
|
887
|
+
"""
|
|
888
|
+
child = indent + " "
|
|
889
|
+
out: list[str] = [f"{indent}<conditionLogic>{x(condition_logic or 'and')}</conditionLogic>\n"]
|
|
890
|
+
for cond in conditions or []:
|
|
891
|
+
out.append(f"{indent}<conditions>\n")
|
|
892
|
+
out.append(f"{child}<leftValueReference>{x(cond.get('leftValueReference') or '')}</leftValueReference>\n")
|
|
893
|
+
out.append(f"{child}<operator>{x(cond.get('operator') or 'EqualTo')}</operator>\n")
|
|
894
|
+
out.append(_render_decision_right_value(cond.get("rightValue"), indent=child))
|
|
895
|
+
out.append(f"{indent}</conditions>\n")
|
|
896
|
+
return "".join(out)
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def render_decisions(decisions: list[dict]) -> str:
|
|
900
|
+
"""Render N <decisions> elements from the spec-level decisions list.
|
|
901
|
+
|
|
902
|
+
Each decision: {name, label, defaultConnector, defaultLabel, rules:[...]}.
|
|
903
|
+
Each rule: {name, label, conditionLogic, conditions:[...], connectsTo}.
|
|
904
|
+
Each condition: {leftValueReference, operator, rightValue: {<type>: v}}.
|
|
905
|
+
"""
|
|
906
|
+
if not decisions:
|
|
907
|
+
return ""
|
|
908
|
+
out: list[str] = []
|
|
909
|
+
for dec in decisions:
|
|
910
|
+
out.append(" <decisions>\n")
|
|
911
|
+
out.append(f" <name>{x(dec.get('name', 'Decision'))}</name>\n")
|
|
912
|
+
out.append(f" <label>{x(dec.get('label') or dec.get('name', 'Decision'))}</label>\n")
|
|
913
|
+
out.append(" <locationX>0</locationX>\n")
|
|
914
|
+
out.append(" <locationY>0</locationY>\n")
|
|
915
|
+
if dec.get("defaultConnector"):
|
|
916
|
+
out.append(" <defaultConnector>\n")
|
|
917
|
+
out.append(f" <targetReference>{x(dec['defaultConnector'])}</targetReference>\n")
|
|
918
|
+
out.append(" </defaultConnector>\n")
|
|
919
|
+
if dec.get("defaultLabel"):
|
|
920
|
+
out.append(f" <defaultConnectorLabel>{x(dec['defaultLabel'])}</defaultConnectorLabel>\n")
|
|
921
|
+
for rule in dec.get("rules") or []:
|
|
922
|
+
out.append(" <rules>\n")
|
|
923
|
+
out.append(f" <name>{x(rule.get('name', 'Rule'))}</name>\n")
|
|
924
|
+
out.append(render_conditions(
|
|
925
|
+
rule.get("conditions") or [],
|
|
926
|
+
rule.get("conditionLogic") or "and",
|
|
927
|
+
indent=" ",
|
|
928
|
+
))
|
|
929
|
+
if rule.get("connectsTo"):
|
|
930
|
+
out.append(" <connector>\n")
|
|
931
|
+
out.append(f" <targetReference>{x(rule['connectsTo'])}</targetReference>\n")
|
|
932
|
+
out.append(" </connector>\n")
|
|
933
|
+
out.append(f" <label>{x(rule.get('label') or rule.get('name', 'Rule'))}</label>\n")
|
|
934
|
+
out.append(" </rules>\n")
|
|
935
|
+
out.append(" </decisions>\n")
|
|
936
|
+
return "".join(out)
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _render_decision_right_value(rv: dict | None, *, indent: str) -> str:
|
|
940
|
+
"""Render <rightValue> from a single-key typed dict."""
|
|
941
|
+
inner = indent + " "
|
|
942
|
+
parts = [f"{indent}<rightValue>\n"]
|
|
943
|
+
rv = rv or {}
|
|
944
|
+
if "elementReference" in rv:
|
|
945
|
+
parts.append(f"{inner}<elementReference>{x(str(rv['elementReference']))}</elementReference>\n")
|
|
946
|
+
elif "booleanValue" in rv:
|
|
947
|
+
parts.append(f"{inner}<booleanValue>{'true' if rv['booleanValue'] else 'false'}</booleanValue>\n")
|
|
948
|
+
elif "numberValue" in rv:
|
|
949
|
+
parts.append(f"{inner}<numberValue>{_fmt_num(float(rv['numberValue']))}</numberValue>\n")
|
|
950
|
+
elif "stringValue" in rv:
|
|
951
|
+
parts.append(f"{inner}<stringValue>{x(str(rv['stringValue']))}</stringValue>\n")
|
|
952
|
+
parts.append(f"{indent}</rightValue>\n")
|
|
953
|
+
return "".join(parts)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def render_post_screen(spec: FormSpec) -> tuple[str, list[str]]:
|
|
957
|
+
"""Returns (xml_chunk, extra_variables_xml_list).
|
|
958
|
+
|
|
959
|
+
extra_variables come from postScreen.variables; the caller appends them
|
|
960
|
+
to the global <variables> block.
|
|
961
|
+
"""
|
|
962
|
+
if not spec.post_screen:
|
|
963
|
+
return "", []
|
|
964
|
+
|
|
965
|
+
out: list[str] = []
|
|
966
|
+
extra_vars_xml: list[str] = []
|
|
967
|
+
|
|
968
|
+
decision = spec.post_screen.get("decision")
|
|
969
|
+
lookups = spec.post_screen.get("recordLookups") or []
|
|
970
|
+
loop = spec.post_screen.get("loop")
|
|
971
|
+
creates = spec.post_screen.get("recordCreates") or []
|
|
972
|
+
variables = spec.post_screen.get("variables") or []
|
|
973
|
+
|
|
974
|
+
# decisions
|
|
975
|
+
if decision and isinstance(decision, dict):
|
|
976
|
+
out.append(" <decisions>\n")
|
|
977
|
+
out.append(f" <name>{x(decision.get('name', 'Decision'))}</name>\n")
|
|
978
|
+
out.append(f" <label>{x(decision.get('label') or decision.get('name', 'Decision'))}</label>\n")
|
|
979
|
+
out.append(" <locationX>0</locationX>\n")
|
|
980
|
+
out.append(" <locationY>0</locationY>\n")
|
|
981
|
+
if decision.get("defaultLabel"):
|
|
982
|
+
out.append(f" <defaultConnectorLabel>{x(decision['defaultLabel'])}</defaultConnectorLabel>\n")
|
|
983
|
+
for branch in decision.get("branches") or []:
|
|
984
|
+
out.append(" <rules>\n")
|
|
985
|
+
out.append(f" <name>{x(branch.get('name', 'Rule'))}</name>\n")
|
|
986
|
+
out.append(" <conditionLogic>and</conditionLogic>\n")
|
|
987
|
+
cond = branch.get("when") or {}
|
|
988
|
+
out.append(" <conditions>\n")
|
|
989
|
+
# When the parent is a choice field, the runtime expects
|
|
990
|
+
# `<fieldName>.selectedChoiceValues` and an elementReference to the choice api-name.
|
|
991
|
+
left_field = cond.get("field", "")
|
|
992
|
+
target_value = cond.get("value", "")
|
|
993
|
+
choice_name = find_choice_name(spec, target_value)
|
|
994
|
+
parent = spec.fields_by_name.get(left_field)
|
|
995
|
+
is_choice = parent is not None and parent.field_type in ("Picklist", "Radio", "CheckboxGroup")
|
|
996
|
+
if is_choice and choice_name:
|
|
997
|
+
out.append(f" <leftValueReference>{x(left_field)}.selectedChoiceValues</leftValueReference>\n")
|
|
998
|
+
out.append(f" <operator>{x(cond.get('operator', 'EqualTo'))}</operator>\n")
|
|
999
|
+
out.append(" <rightValue>\n")
|
|
1000
|
+
out.append(f" <elementReference>{x(choice_name)}</elementReference>\n")
|
|
1001
|
+
out.append(" </rightValue>\n")
|
|
1002
|
+
else:
|
|
1003
|
+
out.append(f" <leftValueReference>{x(left_field)}</leftValueReference>\n")
|
|
1004
|
+
out.append(f" <operator>{x(cond.get('operator', 'EqualTo'))}</operator>\n")
|
|
1005
|
+
out.append(_render_value(spec, cond, indent=" "))
|
|
1006
|
+
out.append(" </conditions>\n")
|
|
1007
|
+
if branch.get("then"):
|
|
1008
|
+
out.append(" <connector>\n")
|
|
1009
|
+
out.append(f" <targetReference>{x(branch['then'])}</targetReference>\n")
|
|
1010
|
+
out.append(" </connector>\n")
|
|
1011
|
+
out.append(f" <label>{x(branch.get('label') or branch.get('name', 'Rule'))}</label>\n")
|
|
1012
|
+
out.append(" </rules>\n")
|
|
1013
|
+
out.append(" </decisions>\n")
|
|
1014
|
+
|
|
1015
|
+
# loops (one)
|
|
1016
|
+
if loop and isinstance(loop, dict):
|
|
1017
|
+
out.append(" <loops>\n")
|
|
1018
|
+
if loop.get("description"):
|
|
1019
|
+
out.append(f" <description>{x(loop['description'])}</description>\n")
|
|
1020
|
+
out.append(f" <name>{x(loop.get('name', 'Loop'))}</name>\n")
|
|
1021
|
+
out.append(f" <label>{x(loop.get('label') or loop.get('name', 'Loop'))}</label>\n")
|
|
1022
|
+
out.append(" <locationX>0</locationX>\n")
|
|
1023
|
+
out.append(" <locationY>0</locationY>\n")
|
|
1024
|
+
out.append(f" <collectionReference>{x(loop.get('collection', ''))}</collectionReference>\n")
|
|
1025
|
+
out.append(" <iterationOrder>Asc</iterationOrder>\n")
|
|
1026
|
+
if loop.get("next"):
|
|
1027
|
+
out.append(" <nextValueConnector>\n")
|
|
1028
|
+
out.append(f" <targetReference>{x(loop['next'])}</targetReference>\n")
|
|
1029
|
+
out.append(" </nextValueConnector>\n")
|
|
1030
|
+
out.append(" </loops>\n")
|
|
1031
|
+
|
|
1032
|
+
# recordCreates
|
|
1033
|
+
for rc in creates:
|
|
1034
|
+
out.append(" <recordCreates>\n")
|
|
1035
|
+
out.append(f" <name>{x(rc.get('name', 'Create'))}</name>\n")
|
|
1036
|
+
out.append(f" <label>{x(rc.get('label') or rc.get('name', 'Create'))}</label>\n")
|
|
1037
|
+
out.append(" <locationX>0</locationX>\n")
|
|
1038
|
+
out.append(" <locationY>0</locationY>\n")
|
|
1039
|
+
if rc.get("next"):
|
|
1040
|
+
out.append(" <connector>\n")
|
|
1041
|
+
out.append(f" <targetReference>{x(rc['next'])}</targetReference>\n")
|
|
1042
|
+
out.append(" </connector>\n")
|
|
1043
|
+
for ia in rc.get("inputAssignments") or []:
|
|
1044
|
+
out.append(" <inputAssignments>\n")
|
|
1045
|
+
out.append(f" <field>{x(ia.get('field', ''))}</field>\n")
|
|
1046
|
+
out.append(_render_value(spec, ia, indent=" "))
|
|
1047
|
+
out.append(" </inputAssignments>\n")
|
|
1048
|
+
out.append(f" <object>{x(rc.get('object', ''))}</object>\n")
|
|
1049
|
+
out.append(" <storeOutputAutomatically>true</storeOutputAutomatically>\n")
|
|
1050
|
+
out.append(" </recordCreates>\n")
|
|
1051
|
+
|
|
1052
|
+
# recordLookups
|
|
1053
|
+
for rl in lookups:
|
|
1054
|
+
out.append(" <recordLookups>\n")
|
|
1055
|
+
out.append(f" <name>{x(rl.get('name', 'Lookup'))}</name>\n")
|
|
1056
|
+
out.append(f" <label>{x(rl.get('label') or rl.get('name', 'Lookup'))}</label>\n")
|
|
1057
|
+
out.append(" <locationX>0</locationX>\n")
|
|
1058
|
+
out.append(" <locationY>0</locationY>\n")
|
|
1059
|
+
out.append(" <assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>\n")
|
|
1060
|
+
if rl.get("next"):
|
|
1061
|
+
out.append(" <connector>\n")
|
|
1062
|
+
out.append(f" <targetReference>{x(rl['next'])}</targetReference>\n")
|
|
1063
|
+
out.append(" </connector>\n")
|
|
1064
|
+
out.append(" <filterLogic>and</filterLogic>\n")
|
|
1065
|
+
for filt in rl.get("filters") or []:
|
|
1066
|
+
out.append(" <filters>\n")
|
|
1067
|
+
out.append(f" <field>{x(filt.get('field', ''))}</field>\n")
|
|
1068
|
+
out.append(f" <operator>{x(filt.get('operator', 'EqualTo'))}</operator>\n")
|
|
1069
|
+
out.append(_render_value(spec, filt, indent=" "))
|
|
1070
|
+
out.append(" </filters>\n")
|
|
1071
|
+
out.append(f" <object>{x(rl.get('object', ''))}</object>\n")
|
|
1072
|
+
for oa in rl.get("outputAssignments") or []:
|
|
1073
|
+
out.append(" <outputAssignments>\n")
|
|
1074
|
+
out.append(f" <assignToReference>{x(oa.get('assignTo', ''))}</assignToReference>\n")
|
|
1075
|
+
out.append(f" <field>{x(oa.get('field', ''))}</field>\n")
|
|
1076
|
+
out.append(" </outputAssignments>\n")
|
|
1077
|
+
out.append(" </recordLookups>\n")
|
|
1078
|
+
|
|
1079
|
+
# extra non-input variables
|
|
1080
|
+
for v in variables:
|
|
1081
|
+
if not isinstance(v, dict):
|
|
1082
|
+
continue
|
|
1083
|
+
name = v.get("name")
|
|
1084
|
+
if not name:
|
|
1085
|
+
continue
|
|
1086
|
+
var_type = v.get("type", "String")
|
|
1087
|
+
extra_vars_xml.append(
|
|
1088
|
+
" <variables>\n"
|
|
1089
|
+
f" <name>{x(name)}</name>\n"
|
|
1090
|
+
f" <dataType>{x(var_type)}</dataType>\n"
|
|
1091
|
+
" <isCollection>false</isCollection>\n"
|
|
1092
|
+
" <isInput>false</isInput>\n"
|
|
1093
|
+
" <isOutput>false</isOutput>\n"
|
|
1094
|
+
" </variables>\n"
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
return "".join(out), extra_vars_xml
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def post_screen_entry_point(spec: FormSpec) -> str | None:
|
|
1101
|
+
"""The element name the last screen connects to, if postScreen is set."""
|
|
1102
|
+
if not spec.post_screen:
|
|
1103
|
+
return None
|
|
1104
|
+
decision = spec.post_screen.get("decision")
|
|
1105
|
+
if decision and decision.get("name"):
|
|
1106
|
+
return decision["name"]
|
|
1107
|
+
lookups = spec.post_screen.get("recordLookups") or []
|
|
1108
|
+
if lookups and lookups[0].get("name"):
|
|
1109
|
+
return lookups[0]["name"]
|
|
1110
|
+
loop = spec.post_screen.get("loop")
|
|
1111
|
+
if loop and loop.get("name"):
|
|
1112
|
+
return loop["name"]
|
|
1113
|
+
creates = spec.post_screen.get("recordCreates") or []
|
|
1114
|
+
if creates and creates[0].get("name"):
|
|
1115
|
+
return creates[0]["name"]
|
|
1116
|
+
return None
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
# ---- top-level XML --------------------------------------------------------
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def build_xml(spec: FormSpec, flow_api_name: str) -> str:
|
|
1123
|
+
if not API_NAME_RE.match(flow_api_name):
|
|
1124
|
+
raise ValueError(
|
|
1125
|
+
f"FlowApiName '{flow_api_name}' is not a valid Salesforce API name "
|
|
1126
|
+
"(must match ^[A-Z][A-Za-z0-9_]*$)"
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
description = f"Generated from spec: {spec.title}"
|
|
1130
|
+
if spec.form_type:
|
|
1131
|
+
description += f" ({spec.form_type})"
|
|
1132
|
+
|
|
1133
|
+
out: list[str] = []
|
|
1134
|
+
out.append('<?xml version="1.0" encoding="UTF-8"?>\n')
|
|
1135
|
+
if spec.incomplete_migration:
|
|
1136
|
+
out.append("<!-- ============================================================\n")
|
|
1137
|
+
out.append(" INCOMPLETE MIGRATION — this DataCaptureFlow was auto-migrated from a\n")
|
|
1138
|
+
out.append(" Field Service Mobile flow. The following source elements have NO Data\n")
|
|
1139
|
+
out.append(" Capture equivalent and were NOT fully migrated. Review and rebuild each\n")
|
|
1140
|
+
out.append(" in Flow Builder before relying on this form.\n")
|
|
1141
|
+
for e in spec.incomplete_migration:
|
|
1142
|
+
etype = _xml_comment_safe(str(e.get("type", "")))
|
|
1143
|
+
name = _xml_comment_safe(str(e.get("name", "")))
|
|
1144
|
+
reason = _xml_comment_safe(str(e.get("reason", "")))
|
|
1145
|
+
out.append(f" - [{etype}] {name}: {reason}\n")
|
|
1146
|
+
out.append(" ============================================================ -->\n")
|
|
1147
|
+
out.append(f'<Flow xmlns="{NS}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n')
|
|
1148
|
+
out.append(" <areMetricsLoggedToDataCloud>false</areMetricsLoggedToDataCloud>\n")
|
|
1149
|
+
|
|
1150
|
+
# Choices block (sorted by name for stable output)
|
|
1151
|
+
for cname in sorted(spec.choices.keys()):
|
|
1152
|
+
out.append(render_choice(cname, spec.choices[cname]))
|
|
1153
|
+
|
|
1154
|
+
# post-screen chunk + extra variables
|
|
1155
|
+
post_xml, extra_vars_xml = render_post_screen(spec)
|
|
1156
|
+
|
|
1157
|
+
out.append(f" <description>{x(description)}</description>\n")
|
|
1158
|
+
out.append(" <environments>Offline</environments>\n")
|
|
1159
|
+
out.append(f" <interviewLabel>{x(spec.title)} {{!$Flow.CurrentDateTime}}</interviewLabel>\n")
|
|
1160
|
+
out.append(f" <label>{x(spec.title)}</label>\n")
|
|
1161
|
+
out.append(" <processMetadataValues>\n")
|
|
1162
|
+
out.append(" <name>BuilderType</name>\n")
|
|
1163
|
+
out.append(" <value>\n")
|
|
1164
|
+
out.append(" <stringValue>LightningFlowBuilder</stringValue>\n")
|
|
1165
|
+
out.append(" </value>\n")
|
|
1166
|
+
out.append(" </processMetadataValues>\n")
|
|
1167
|
+
out.append(" <processMetadataValues>\n")
|
|
1168
|
+
out.append(" <name>CanvasMode</name>\n")
|
|
1169
|
+
out.append(" <value>\n")
|
|
1170
|
+
out.append(" <stringValue>AUTO_LAYOUT_CANVAS</stringValue>\n")
|
|
1171
|
+
out.append(" </value>\n")
|
|
1172
|
+
out.append(" </processMetadataValues>\n")
|
|
1173
|
+
out.append(" <processType>DataCaptureFlow</processType>\n")
|
|
1174
|
+
|
|
1175
|
+
# post-screen elements (decisions / loops / lookups / creates) appear before screens
|
|
1176
|
+
out.append(post_xml)
|
|
1177
|
+
out.append(render_decisions(spec.decisions))
|
|
1178
|
+
|
|
1179
|
+
# Screens
|
|
1180
|
+
post_entry = post_screen_entry_point(spec)
|
|
1181
|
+
for i, screen in enumerate(spec.screens):
|
|
1182
|
+
is_last = i == len(spec.screens) - 1
|
|
1183
|
+
# A source screen connector (to a decision or another screen) overrides fall-through.
|
|
1184
|
+
override = spec.screen_connectors.get(screen.label) or spec.screen_connectors.get(screen.name)
|
|
1185
|
+
if override:
|
|
1186
|
+
next_target = override
|
|
1187
|
+
elif is_last:
|
|
1188
|
+
next_target = post_entry
|
|
1189
|
+
else:
|
|
1190
|
+
next_target = spec.screens[i + 1].name
|
|
1191
|
+
out.append(render_screen(screen, spec, next_target=next_target))
|
|
1192
|
+
|
|
1193
|
+
# Start
|
|
1194
|
+
first_screen = spec.screens[0]
|
|
1195
|
+
out.append(" <start>\n")
|
|
1196
|
+
out.append(" <locationX>0</locationX>\n")
|
|
1197
|
+
out.append(" <locationY>0</locationY>\n")
|
|
1198
|
+
out.append(" <connector>\n")
|
|
1199
|
+
out.append(f" <targetReference>{x(first_screen.name)}</targetReference>\n")
|
|
1200
|
+
out.append(" </connector>\n")
|
|
1201
|
+
out.append(" </start>\n")
|
|
1202
|
+
|
|
1203
|
+
out.append(" <status>Draft</status>\n")
|
|
1204
|
+
# Required input variables for any DataCaptureFlow
|
|
1205
|
+
for var_name in ("parentObjectType", "parentRecordId", "recordId"):
|
|
1206
|
+
out.append(" <variables>\n")
|
|
1207
|
+
out.append(f" <name>{var_name}</name>\n")
|
|
1208
|
+
out.append(" <dataType>String</dataType>\n")
|
|
1209
|
+
out.append(" <isCollection>false</isCollection>\n")
|
|
1210
|
+
out.append(" <isInput>true</isInput>\n")
|
|
1211
|
+
out.append(" <isOutput>false</isOutput>\n")
|
|
1212
|
+
out.append(" </variables>\n")
|
|
1213
|
+
for v in extra_vars_xml:
|
|
1214
|
+
out.append(v)
|
|
1215
|
+
out.append("</Flow>\n")
|
|
1216
|
+
return "".join(out)
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def main(argv: list[str]) -> int:
|
|
1220
|
+
if len(argv) != 4:
|
|
1221
|
+
print(__doc__)
|
|
1222
|
+
return 2
|
|
1223
|
+
in_path = Path(argv[1])
|
|
1224
|
+
flow_api_name = argv[2]
|
|
1225
|
+
out_path = Path(argv[3])
|
|
1226
|
+
|
|
1227
|
+
data = json.loads(in_path.read_text(encoding="utf-8"))
|
|
1228
|
+
spec = parse_spec(data)
|
|
1229
|
+
xml = build_xml(spec, flow_api_name)
|
|
1230
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1231
|
+
out_path.write_text(xml, encoding="utf-8")
|
|
1232
|
+
|
|
1233
|
+
total_fields = sum(1 for _ in _walk_fields([f for s in spec.screens for f in s.fields]))
|
|
1234
|
+
section_count = sum(len(s.sections or []) for s in spec.screens)
|
|
1235
|
+
print(f"Wrote {out_path}")
|
|
1236
|
+
print(f" Flow: {flow_api_name}")
|
|
1237
|
+
print(f" Title: {spec.title}")
|
|
1238
|
+
print(f" Screens: {len(spec.screens)}")
|
|
1239
|
+
if section_count:
|
|
1240
|
+
print(f" Sections: {section_count}")
|
|
1241
|
+
print(f" Fields total (incl. repeater children): {total_fields}")
|
|
1242
|
+
print(f" Choices: {len(spec.choices)}")
|
|
1243
|
+
if spec.post_screen:
|
|
1244
|
+
ps = spec.post_screen
|
|
1245
|
+
print(f" postScreen: decision={'yes' if ps.get('decision') else 'no'}, "
|
|
1246
|
+
f"lookups={len(ps.get('recordLookups') or [])}, "
|
|
1247
|
+
f"loop={'yes' if ps.get('loop') else 'no'}, "
|
|
1248
|
+
f"creates={len(ps.get('recordCreates') or [])}, "
|
|
1249
|
+
f"vars={len(ps.get('variables') or [])}")
|
|
1250
|
+
if spec.warnings:
|
|
1251
|
+
print(" Warnings:")
|
|
1252
|
+
for w in spec.warnings:
|
|
1253
|
+
print(f" - {w}")
|
|
1254
|
+
return 0
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
if __name__ == "__main__":
|
|
1258
|
+
sys.exit(main(sys.argv))
|