@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,1661 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Fully automated FieldServiceMobile → DataCaptureFlow transformer.
|
|
4
|
+
|
|
5
|
+
Handles every known migration issue:
|
|
6
|
+
1. processType, environments, apiVersion, areMetricsLoggedToDataCloud
|
|
7
|
+
2. Screen field type conversion:
|
|
8
|
+
InputField → ComponentInstance (dc* component by dataType)
|
|
9
|
+
DropdownBox → ComponentChoice + dcPicklist
|
|
10
|
+
RadioButtons → ComponentChoice + dcRbGroup
|
|
11
|
+
MultiSelectCheckboxes → ComponentMultiChoice + dcCbGroup
|
|
12
|
+
LargeTextArea → ComponentInstance + dcLongText
|
|
13
|
+
forceContent:fileUpload → ComponentInstance + dcUpFile
|
|
14
|
+
3. outputAssignment removal: replaced with storeOutputAutomatically=true
|
|
15
|
+
+ an explicit <assignments> node after the screen to preserve variable bindings
|
|
16
|
+
4. CUD-at-end enforcement (the main structural challenge):
|
|
17
|
+
Pattern A: CUD → Screen → flip (screen first, then CUD)
|
|
18
|
+
Pattern B: CUD → Lookup/Assignment → hoist lookup to pre-screen phase
|
|
19
|
+
Pattern C: CUD → Decision (mixed) → hoist decision to pre-screen phase
|
|
20
|
+
5. Remove Constants (not allowed in DC)
|
|
21
|
+
6. Remove invalid field attributes (defaultValue, dataType on ComponentChoice etc.)
|
|
22
|
+
7. Deduplicate variables (case-insensitive); fix isInput on required DC vars
|
|
23
|
+
8. Ensure required screen attributes (allowFinish, showFooter, nextOrFinishButtonLabel)
|
|
24
|
+
9. Ensure required DC input variables (recordId, parentRecordId, parentObjectType)
|
|
25
|
+
10. Reorder field children so inputParameters are contiguous
|
|
26
|
+
11. Update subflow flowName references to DC version (appends _DC suffix)
|
|
27
|
+
12. Detects flows with no screens (pure automation) and skips with explanation
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
python3 transform_flow.py <input.flow-meta.xml> <output.flow-meta.xml> [--dc-suffix _DC]
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import sys
|
|
34
|
+
import re
|
|
35
|
+
import copy
|
|
36
|
+
import xml.etree.ElementTree as ET
|
|
37
|
+
from collections import defaultdict, OrderedDict
|
|
38
|
+
from typing import Optional, Dict, List, Tuple, Set
|
|
39
|
+
from subflow_names import migrated_dc_flow_name
|
|
40
|
+
from manual_review_flags import detect_manual_review_flags, build_excision_flag
|
|
41
|
+
|
|
42
|
+
NS_URI = 'http://soap.sforce.com/2006/04/metadata'
|
|
43
|
+
NS = {'sf': NS_URI}
|
|
44
|
+
ET.register_namespace('', NS_URI)
|
|
45
|
+
|
|
46
|
+
CUD_TYPES = frozenset(('recordCreates', 'recordUpdates', 'recordDeletes'))
|
|
47
|
+
ALL_TYPES = ('screens', 'decisions', 'recordLookups', 'recordCreates',
|
|
48
|
+
'recordUpdates', 'recordDeletes', 'loops', 'assignments', 'subflows',
|
|
49
|
+
'actionCalls')
|
|
50
|
+
|
|
51
|
+
# ──────────────────────────────────────────────
|
|
52
|
+
# XML helpers
|
|
53
|
+
# ──────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def tag(n: str) -> str:
|
|
56
|
+
return f'{{{NS_URI}}}{n}'
|
|
57
|
+
|
|
58
|
+
def local(el: ET.Element) -> str:
|
|
59
|
+
return el.tag.split('}')[-1]
|
|
60
|
+
|
|
61
|
+
def findall(el: ET.Element, n: str) -> List[ET.Element]:
|
|
62
|
+
r = el.findall(f'sf:{n}', namespaces=NS)
|
|
63
|
+
return r if r else el.findall(n)
|
|
64
|
+
|
|
65
|
+
def findtext(el: ET.Element, n: str) -> Optional[str]:
|
|
66
|
+
r = el.findtext(f'sf:{n}', namespaces=NS)
|
|
67
|
+
return r if r is not None else el.findtext(n)
|
|
68
|
+
|
|
69
|
+
def make(name: str, text: str = None) -> ET.Element:
|
|
70
|
+
el = ET.Element(tag(name))
|
|
71
|
+
if text is not None:
|
|
72
|
+
el.text = text
|
|
73
|
+
return el
|
|
74
|
+
|
|
75
|
+
def sub(parent: ET.Element, name: str, text: str = None) -> ET.Element:
|
|
76
|
+
el = ET.SubElement(parent, tag(name))
|
|
77
|
+
if text is not None:
|
|
78
|
+
el.text = text
|
|
79
|
+
return el
|
|
80
|
+
|
|
81
|
+
# ──────────────────────────────────────────────
|
|
82
|
+
# Graph helpers
|
|
83
|
+
# ──────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def get_connector_targets(node_el: ET.Element) -> List[str]:
|
|
86
|
+
targets = []
|
|
87
|
+
for conn in (findall(node_el, 'connector') +
|
|
88
|
+
findall(node_el, 'defaultConnector') +
|
|
89
|
+
findall(node_el, 'nextValueConnector') +
|
|
90
|
+
findall(node_el, 'noMoreValuesConnector') +
|
|
91
|
+
findall(node_el, 'faultConnector')):
|
|
92
|
+
t = findtext(conn, 'targetReference')
|
|
93
|
+
if t:
|
|
94
|
+
targets.append(t)
|
|
95
|
+
for rule in findall(node_el, 'rules'):
|
|
96
|
+
for conn in findall(rule, 'connector'):
|
|
97
|
+
t = findtext(conn, 'targetReference')
|
|
98
|
+
if t:
|
|
99
|
+
targets.append(t)
|
|
100
|
+
return targets
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_fault_targets(node_el: ET.Element) -> List[str]:
|
|
104
|
+
"""Return the targetReferences reached via <faultConnector> only.
|
|
105
|
+
|
|
106
|
+
Fault edges are still real graph edges (Salesforce enforces CUD-at-end on
|
|
107
|
+
them), but they need separate handling from success edges: a fault handler
|
|
108
|
+
cannot be hoisted "before" the CUD, so the four hoist patterns don't apply.
|
|
109
|
+
"""
|
|
110
|
+
targets = []
|
|
111
|
+
for conn in findall(node_el, 'faultConnector'):
|
|
112
|
+
t = findtext(conn, 'targetReference')
|
|
113
|
+
if t:
|
|
114
|
+
targets.append(t)
|
|
115
|
+
return targets
|
|
116
|
+
|
|
117
|
+
def build_graph(root: ET.Element) -> Tuple[Dict, Optional[str]]:
|
|
118
|
+
nodes: Dict[str, Tuple[str, ET.Element, List[str]]] = {}
|
|
119
|
+
for ntype in ALL_TYPES:
|
|
120
|
+
for node in findall(root, ntype):
|
|
121
|
+
nname = findtext(node, 'name')
|
|
122
|
+
if nname:
|
|
123
|
+
nodes[nname] = (ntype, node, get_connector_targets(node))
|
|
124
|
+
start_target = None
|
|
125
|
+
for s in findall(root, 'start'):
|
|
126
|
+
for conn in findall(s, 'connector'):
|
|
127
|
+
start_target = findtext(conn, 'targetReference')
|
|
128
|
+
return nodes, start_target
|
|
129
|
+
|
|
130
|
+
def bfs_order(nodes: Dict, start: Optional[str]) -> List[str]:
|
|
131
|
+
visited, order = set(), []
|
|
132
|
+
q = [start] if start else []
|
|
133
|
+
while q:
|
|
134
|
+
n = q.pop(0)
|
|
135
|
+
if not n or n in visited:
|
|
136
|
+
continue
|
|
137
|
+
visited.add(n)
|
|
138
|
+
order.append(n)
|
|
139
|
+
if n in nodes:
|
|
140
|
+
for t in nodes[n][2]:
|
|
141
|
+
if t not in visited:
|
|
142
|
+
q.append(t)
|
|
143
|
+
return order
|
|
144
|
+
|
|
145
|
+
def find_violations(nodes: Dict) -> List[Tuple[str, str]]:
|
|
146
|
+
"""Return [(type, name)] for every non-CUD node reached by a direct connector
|
|
147
|
+
edge from a CUD node.
|
|
148
|
+
|
|
149
|
+
Edge-based, not order-based: single-visit BFS records only one discovery
|
|
150
|
+
position per node, so a CUD→node edge is invisible when that same node is
|
|
151
|
+
also reachable earlier via a non-CUD path (a diamond). Inspecting each CUD
|
|
152
|
+
node's actual connectors catches the edge regardless of BFS discovery order.
|
|
153
|
+
"""
|
|
154
|
+
violations = []
|
|
155
|
+
seen = set()
|
|
156
|
+
for _name, (ntype, _el, targets) in nodes.items():
|
|
157
|
+
if ntype not in CUD_TYPES:
|
|
158
|
+
continue
|
|
159
|
+
for t in targets:
|
|
160
|
+
if t in nodes and nodes[t][0] not in CUD_TYPES:
|
|
161
|
+
key = (nodes[t][0], t)
|
|
162
|
+
if key not in seen:
|
|
163
|
+
seen.add(key)
|
|
164
|
+
violations.append(key)
|
|
165
|
+
return violations
|
|
166
|
+
|
|
167
|
+
def update_all_connectors(root: ET.Element, old: str, new: str) -> int:
|
|
168
|
+
count = 0
|
|
169
|
+
for ntype in ALL_TYPES + ('start',):
|
|
170
|
+
for node in findall(root, ntype):
|
|
171
|
+
for conn in (findall(node, 'connector') +
|
|
172
|
+
findall(node, 'defaultConnector') +
|
|
173
|
+
findall(node, 'nextValueConnector') +
|
|
174
|
+
findall(node, 'noMoreValuesConnector')):
|
|
175
|
+
for tr in findall(conn, 'targetReference'):
|
|
176
|
+
if tr.text == old:
|
|
177
|
+
tr.text = new
|
|
178
|
+
count += 1
|
|
179
|
+
for rule in findall(node, 'rules'):
|
|
180
|
+
for conn in findall(rule, 'connector'):
|
|
181
|
+
for tr in findall(conn, 'targetReference'):
|
|
182
|
+
if tr.text == old:
|
|
183
|
+
tr.text = new
|
|
184
|
+
count += 1
|
|
185
|
+
return count
|
|
186
|
+
|
|
187
|
+
def get_start_target(root: ET.Element) -> Optional[str]:
|
|
188
|
+
for s in findall(root, 'start'):
|
|
189
|
+
for conn in findall(s, 'connector'):
|
|
190
|
+
return findtext(conn, 'targetReference')
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def set_start_target(root: ET.Element, new_target: str):
|
|
194
|
+
for s in findall(root, 'start'):
|
|
195
|
+
for conn in findall(s, 'connector'):
|
|
196
|
+
for tr in findall(conn, 'targetReference'):
|
|
197
|
+
tr.text = new_target
|
|
198
|
+
|
|
199
|
+
def predecessors_of(root: ET.Element, name: str) -> List[str]:
|
|
200
|
+
preds = []
|
|
201
|
+
start_t = get_start_target(root)
|
|
202
|
+
if start_t == name:
|
|
203
|
+
preds.append('__start__')
|
|
204
|
+
for ntype in ALL_TYPES:
|
|
205
|
+
for node in findall(root, ntype):
|
|
206
|
+
nname = findtext(node, 'name')
|
|
207
|
+
if name in get_connector_targets(node):
|
|
208
|
+
preds.append(nname)
|
|
209
|
+
return preds
|
|
210
|
+
|
|
211
|
+
# ──────────────────────────────────────────────
|
|
212
|
+
# Component type mappings
|
|
213
|
+
# ──────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
# (fieldType, dataType) → (new_fieldType, extensionName, label_style)
|
|
216
|
+
# label_style: 'param'=inputParameter 'fieldtext'=<fieldText> 'none'=skip
|
|
217
|
+
FIELD_TYPE_MAP = {
|
|
218
|
+
# ComponentChoice types (use fieldText for label, keep extensionName)
|
|
219
|
+
('DropdownBox', None): ('ComponentChoice', 'runtime_service_fieldservice:dcPicklist', 'fieldtext'),
|
|
220
|
+
('RadioButtons', None): ('ComponentChoice', 'runtime_service_fieldservice:dcRbGroup', 'fieldtext'),
|
|
221
|
+
('RadioButtonGroup', None): ('ComponentChoice', 'runtime_service_fieldservice:dcRbGroup', 'fieldtext'),
|
|
222
|
+
('MultiSelectCheckboxes',None): ('ComponentMultiChoice', 'runtime_service_fieldservice:dcCbGroup', 'fieldtext'),
|
|
223
|
+
('MultiSelectPicklist', None): ('ComponentMultiChoice', 'runtime_service_fieldservice:dcCbGroup', 'fieldtext'),
|
|
224
|
+
# InputField → ComponentInstance based on dataType
|
|
225
|
+
('InputField', 'Boolean'): ('ComponentInstance', 'runtime_service_fieldservice:dcCheckbox', 'param'),
|
|
226
|
+
('InputField', 'String'): ('ComponentInstance', 'runtime_service_fieldservice:dcTextInput', 'param'),
|
|
227
|
+
('InputField', 'Number'): ('ComponentInstance', 'runtime_service_fieldservice:dcNumeric', 'param'),
|
|
228
|
+
('InputField', 'Double'): ('ComponentInstance', 'runtime_service_fieldservice:dcNumeric', 'param'),
|
|
229
|
+
('InputField', 'Currency'): ('ComponentInstance', 'runtime_service_fieldservice:dcNumeric', 'param'),
|
|
230
|
+
('InputField', 'Date'): ('ComponentInstance', 'runtime_service_fieldservice:dcDate', 'param'),
|
|
231
|
+
('InputField', 'DateTime'): ('ComponentInstance', 'runtime_service_fieldservice:dcDateTime', 'param'),
|
|
232
|
+
('InputField', 'Email'): ('ComponentInstance', 'runtime_service_fieldservice:dcEmail', 'param'),
|
|
233
|
+
('InputField', 'Phone'): ('ComponentInstance', 'runtime_service_fieldservice:dcPhone', 'param'),
|
|
234
|
+
('InputField', 'Picklist'): ('ComponentChoice', 'runtime_service_fieldservice:dcPicklist', 'fieldtext'),
|
|
235
|
+
('InputField', 'MultiselectPicklist'):('ComponentMultiChoice','runtime_service_fieldservice:dcCbGroup','fieldtext'),
|
|
236
|
+
('InputField', 'TextArea'): ('ComponentInstance', 'runtime_service_fieldservice:dcLongText', 'param'),
|
|
237
|
+
('InputField', 'LongTextArea'): ('ComponentInstance', 'runtime_service_fieldservice:dcLongText', 'param'),
|
|
238
|
+
('InputField', 'Name'): ('ComponentInstance', 'runtime_service_fieldservice:dcName', 'param'),
|
|
239
|
+
('InputField', 'Toggle'): ('ComponentInstance', 'runtime_service_fieldservice:dcToggle', 'param'),
|
|
240
|
+
('InputField', 'Address'): ('ComponentInstance', 'runtime_service_fieldservice:dcAddress', 'param'),
|
|
241
|
+
('InputField', 'Lookup'): ('ComponentInstance', 'runtime_service_fieldservice:dcLookup', 'param'),
|
|
242
|
+
('InputField', 'Counter'): ('ComponentInstance', 'runtime_service_fieldservice:dcCounter', 'param'),
|
|
243
|
+
('InputField', 'Matrix'): ('ComponentMultiChoice','runtime_service_fieldservice:dcMatrix', 'fieldtext'),
|
|
244
|
+
('InputField', None): ('ComponentInstance', 'runtime_service_fieldservice:dcTextInput', 'param'),
|
|
245
|
+
# LargeTextArea
|
|
246
|
+
('LargeTextArea', None): ('ComponentInstance', 'runtime_service_fieldservice:dcLongText', 'param'),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
EXTENSION_MAP = {
|
|
250
|
+
'forceContent:fileUpload': ('ComponentInstance', 'runtime_service_fieldservice:dcUpFile', 'param'),
|
|
251
|
+
'forceContent:imageUpload': ('ComponentInstance', 'runtime_service_fieldservice:dcUpImage', 'param'),
|
|
252
|
+
'forceContent:fileView': ('ComponentInstance', 'runtime_service_fieldservice:dcFileView', 'param'),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Required inputParameters for ComponentChoice types (must be REMOVED)
|
|
256
|
+
CHOICE_INVALID_ATTRS = frozenset((
|
|
257
|
+
'dataType', 'defaultValue', 'storeOutputAutomatically',
|
|
258
|
+
'inputsOnNextNavToAssocScrn', 'styleProperties', 'inputParameters',
|
|
259
|
+
))
|
|
260
|
+
|
|
261
|
+
# Required attributes for ComponentChoice (must be PRESENT)
|
|
262
|
+
CHOICE_REQUIRED = {
|
|
263
|
+
'storeOutputAutomatically': 'true',
|
|
264
|
+
'inputsOnNextNavToAssocScrn': 'UseStoredValues',
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# Invalid inputParameter names on dc* components
|
|
268
|
+
INVALID_PARAMS = frozenset(('required', 'disabled', 'multiple', 'accept', 'helpText', 'placeholder'))
|
|
269
|
+
|
|
270
|
+
# ──────────────────────────────────────────────
|
|
271
|
+
# Phase 1: Screen field transformation
|
|
272
|
+
# ──────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def transform_screen_fields(root: ET.Element) -> Tuple[int, Dict[str, str]]:
|
|
275
|
+
"""
|
|
276
|
+
Convert all screen fields to DC-compatible format.
|
|
277
|
+
Returns (fields_converted, output_var_map) where output_var_map maps
|
|
278
|
+
old outputAssignment variable names to new {fieldName.value} references.
|
|
279
|
+
"""
|
|
280
|
+
converted = 0
|
|
281
|
+
output_var_map: Dict[str, str] = {} # old_var → new_reference
|
|
282
|
+
|
|
283
|
+
for screen in findall(root, 'screens'):
|
|
284
|
+
screen_name = findtext(screen, 'name') or 'Screen'
|
|
285
|
+
for field in findall(screen, 'fields'):
|
|
286
|
+
ft = findtext(field, 'fieldType') or ''
|
|
287
|
+
ext = findtext(field, 'extensionName') or ''
|
|
288
|
+
dt = findtext(field, 'dataType')
|
|
289
|
+
fname = findtext(field, 'name') or 'field'
|
|
290
|
+
|
|
291
|
+
# Collect outputAssignments before removing them
|
|
292
|
+
for oa in findall(field, 'outputAssignments'):
|
|
293
|
+
assign_to = findtext(oa, 'assignToReference')
|
|
294
|
+
output_name = findtext(oa, 'name') or 'value'
|
|
295
|
+
if assign_to:
|
|
296
|
+
# Detect duplicate assignments to same variable
|
|
297
|
+
if assign_to in output_var_map:
|
|
298
|
+
print(f" WARNING: Multiple fields assign to variable '{assign_to}'. "
|
|
299
|
+
f"Previous: {output_var_map[assign_to]}, Now: {fname}.{output_name}. "
|
|
300
|
+
f"Only the last assignment will be preserved.", file=sys.stderr)
|
|
301
|
+
|
|
302
|
+
# Map old variable → new DC accessor
|
|
303
|
+
if output_name in ('value',):
|
|
304
|
+
output_var_map[assign_to] = f'{fname}.value'
|
|
305
|
+
elif output_name in ('selectedChoiceValues',):
|
|
306
|
+
output_var_map[assign_to] = f'{fname}.selectedChoiceValues'
|
|
307
|
+
else:
|
|
308
|
+
output_var_map[assign_to] = f'{fname}.{output_name}'
|
|
309
|
+
field.remove(oa)
|
|
310
|
+
|
|
311
|
+
# Already a DC component — just clean up invalid attributes
|
|
312
|
+
if ext.startswith('runtime_service_fieldservice:dc'):
|
|
313
|
+
_clean_dc_field(field, ft)
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
# DisplayText — pass through, just remove outputAssignments (done above)
|
|
317
|
+
if ft == 'DisplayText':
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
# Repeater — pass through
|
|
321
|
+
if ft == 'Repeater':
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
# RegionContainer / Region — pass through
|
|
325
|
+
if ft in ('RegionContainer', 'Region'):
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Check extension map first
|
|
329
|
+
if ext in EXTENSION_MAP:
|
|
330
|
+
new_ft, new_ext, label_style = EXTENSION_MAP[ext]
|
|
331
|
+
_apply_component_type(field, new_ft, new_ext, label_style, fname)
|
|
332
|
+
converted += 1
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
# Look up in field type map
|
|
336
|
+
key = (ft, dt) if (ft, dt) in FIELD_TYPE_MAP else (ft, None)
|
|
337
|
+
if key in FIELD_TYPE_MAP:
|
|
338
|
+
new_ft, new_ext, label_style = FIELD_TYPE_MAP[key]
|
|
339
|
+
_apply_component_type(field, new_ft, new_ext, label_style, fname)
|
|
340
|
+
converted += 1
|
|
341
|
+
# else: unknown type — leave as-is, deploy will surface the error
|
|
342
|
+
|
|
343
|
+
return converted, output_var_map
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _apply_component_type(field: ET.Element, new_ft: str, new_ext: str, label_style: str, fname: str):
|
|
347
|
+
"""Set fieldType, extensionName, label, and required attributes on a field."""
|
|
348
|
+
field_text = findtext(field, 'fieldText') or fname
|
|
349
|
+
|
|
350
|
+
# Remove elements we're replacing
|
|
351
|
+
for bad in ('fieldType', 'extensionName', 'dataType', 'defaultValue'):
|
|
352
|
+
for el in findall(field, bad):
|
|
353
|
+
field.remove(el)
|
|
354
|
+
|
|
355
|
+
# Remove invalid inputParameters
|
|
356
|
+
for ip in findall(field, 'inputParameters'):
|
|
357
|
+
pname = findtext(ip, 'name')
|
|
358
|
+
if pname in INVALID_PARAMS:
|
|
359
|
+
field.remove(ip)
|
|
360
|
+
|
|
361
|
+
# Set new fieldType and extensionName
|
|
362
|
+
ft_el = make('fieldType', new_ft)
|
|
363
|
+
ext_el = make('extensionName', new_ext)
|
|
364
|
+
|
|
365
|
+
# Insert at beginning (after name)
|
|
366
|
+
name_el = findall(field, 'name')
|
|
367
|
+
insert_pos = 1 if name_el else 0
|
|
368
|
+
field.insert(insert_pos, ext_el)
|
|
369
|
+
field.insert(insert_pos, ft_el)
|
|
370
|
+
|
|
371
|
+
if new_ft == 'ComponentInstance':
|
|
372
|
+
_clean_dc_field(field, new_ft)
|
|
373
|
+
# Ensure label inputParameter
|
|
374
|
+
has_label = any(findtext(p, 'name') == 'label' for p in findall(field, 'inputParameters'))
|
|
375
|
+
if not has_label and label_style == 'param':
|
|
376
|
+
ip = make('inputParameters')
|
|
377
|
+
ip.append(make('name', 'label'))
|
|
378
|
+
v = sub(ip, 'value')
|
|
379
|
+
v.append(make('stringValue', field_text))
|
|
380
|
+
field.append(ip)
|
|
381
|
+
|
|
382
|
+
# dc* ComponentInstance fields are LWC extension components that read
|
|
383
|
+
# their own config from inputParameters, not from the flow-level
|
|
384
|
+
# <isRequired> flag — Flow Builder itself emits a `required`
|
|
385
|
+
# inputParameter alongside <isRequired> for these fields, and without
|
|
386
|
+
# it the DC runtime silently treats the field as optional even though
|
|
387
|
+
# <isRequired>true</isRequired> is present and Flow Builder's preview
|
|
388
|
+
# shows it as required. (ComponentChoice/ComponentMultiChoice fields
|
|
389
|
+
# don't need this — they read <isRequired> directly.)
|
|
390
|
+
is_required = (findtext(field, 'isRequired') or 'false').lower() == 'true'
|
|
391
|
+
has_required_param = any(findtext(p, 'name') == 'required' for p in findall(field, 'inputParameters'))
|
|
392
|
+
if not has_required_param:
|
|
393
|
+
ip = make('inputParameters')
|
|
394
|
+
ip.append(make('name', 'required'))
|
|
395
|
+
v = sub(ip, 'value')
|
|
396
|
+
v.append(make('booleanValue', 'true' if is_required else 'false'))
|
|
397
|
+
field.append(ip)
|
|
398
|
+
|
|
399
|
+
elif new_ft in ('ComponentChoice', 'ComponentMultiChoice'):
|
|
400
|
+
# Remove invalid attributes
|
|
401
|
+
for bad in ('storeOutputAutomatically', 'inputsOnNextNavToAssocScrn',
|
|
402
|
+
'inputParameters', 'styleProperties'):
|
|
403
|
+
for el in findall(field, bad):
|
|
404
|
+
field.remove(el)
|
|
405
|
+
# Ensure fieldText (label for choice components)
|
|
406
|
+
if findtext(field, 'fieldText') is None:
|
|
407
|
+
field.append(make('fieldText', field_text))
|
|
408
|
+
# Add required attributes
|
|
409
|
+
field.append(make('storeOutputAutomatically', 'true'))
|
|
410
|
+
field.append(make('inputsOnNextNavToAssocScrn', 'UseStoredValues'))
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _clean_dc_field(field: ET.Element, ft: str):
|
|
414
|
+
"""Clean a ComponentInstance field: remove invalid attrs, add required ones."""
|
|
415
|
+
# Remove invalid inputParameters
|
|
416
|
+
for ip in findall(field, 'inputParameters'):
|
|
417
|
+
if findtext(ip, 'name') in INVALID_PARAMS:
|
|
418
|
+
field.remove(ip)
|
|
419
|
+
|
|
420
|
+
# Remove defaultValue (DC uses value inputParameter)
|
|
421
|
+
for el in findall(field, 'defaultValue'):
|
|
422
|
+
field.remove(el)
|
|
423
|
+
|
|
424
|
+
if ft == 'ComponentInstance':
|
|
425
|
+
# dc* ComponentInstance fields render their own label from the
|
|
426
|
+
# `label` inputParameter; a leftover <fieldText> from the source
|
|
427
|
+
# InputField/etc. field breaks rendering at DC runtime (screen gets
|
|
428
|
+
# stuck with no Next/footer) even though it deploys and previews
|
|
429
|
+
# fine in Flow Builder.
|
|
430
|
+
for el in findall(field, 'fieldText'):
|
|
431
|
+
field.remove(el)
|
|
432
|
+
|
|
433
|
+
if findtext(field, 'storeOutputAutomatically') is None:
|
|
434
|
+
field.append(make('storeOutputAutomatically', 'true'))
|
|
435
|
+
if findtext(field, 'inputsOnNextNavToAssocScrn') is None:
|
|
436
|
+
field.append(make('inputsOnNextNavToAssocScrn', 'UseStoredValues'))
|
|
437
|
+
if findtext(field, 'styleProperties') is None:
|
|
438
|
+
sp = make('styleProperties')
|
|
439
|
+
va = sub(sp, 'verticalAlignment')
|
|
440
|
+
va.append(make('stringValue', 'top'))
|
|
441
|
+
w = sub(sp, 'width')
|
|
442
|
+
w.append(make('stringValue', '12'))
|
|
443
|
+
field.append(sp)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def regroup_root_children(root: ET.Element):
|
|
447
|
+
"""Regroup root's direct children so each element type is contiguous.
|
|
448
|
+
|
|
449
|
+
excise_and_placeholder() always root.append()s new placeholder <screens>
|
|
450
|
+
(used by CUD excision, ACTION_CALL_EXCISED, and PLATFORM_EVENT_EXCISED),
|
|
451
|
+
landing them after whatever tag currently sits last (subflows, variables,
|
|
452
|
+
...) — breaking contiguity with the flow's original <screens> block and
|
|
453
|
+
causing the deploy error "Element screens is duplicated at this location".
|
|
454
|
+
Mirrors the variables regroup in ensure_dc_variables, generalized to any
|
|
455
|
+
tag, and run once at the very end so it catches every excision site
|
|
456
|
+
regardless of which phase ran it.
|
|
457
|
+
"""
|
|
458
|
+
children = list(root)
|
|
459
|
+
grouped: OrderedDict = OrderedDict()
|
|
460
|
+
for child in children:
|
|
461
|
+
lname = local(child)
|
|
462
|
+
grouped.setdefault(lname, []).append(child)
|
|
463
|
+
for child in children:
|
|
464
|
+
root.remove(child)
|
|
465
|
+
for children_list in grouped.values():
|
|
466
|
+
for child in children_list:
|
|
467
|
+
root.append(child)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def reorder_field_children(root: ET.Element):
|
|
471
|
+
"""Reorder screen field children so inputParameters are contiguous."""
|
|
472
|
+
ORDER = ['name', 'extensionName', 'fieldType', 'fieldText', 'choiceReferences',
|
|
473
|
+
'dynamicChoicesSets', 'isRequired', 'inputParameters',
|
|
474
|
+
'storeOutputAutomatically', 'inputsOnNextNavToAssocScrn',
|
|
475
|
+
'validationRule', 'visibilityRule', 'styleProperties']
|
|
476
|
+
|
|
477
|
+
for screen in findall(root, 'screens'):
|
|
478
|
+
for field in findall(screen, 'fields'):
|
|
479
|
+
children = list(field)
|
|
480
|
+
if not children:
|
|
481
|
+
continue
|
|
482
|
+
grouped: OrderedDict = OrderedDict()
|
|
483
|
+
for child in children:
|
|
484
|
+
lname = local(child)
|
|
485
|
+
if lname not in grouped:
|
|
486
|
+
grouped[lname] = []
|
|
487
|
+
grouped[lname].append(child)
|
|
488
|
+
for child in children:
|
|
489
|
+
field.remove(child)
|
|
490
|
+
for key in ORDER:
|
|
491
|
+
for child in grouped.pop(key, []):
|
|
492
|
+
field.append(child)
|
|
493
|
+
for children_list in grouped.values():
|
|
494
|
+
for child in children_list:
|
|
495
|
+
field.append(child)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# ──────────────────────────────────────────────
|
|
499
|
+
# Phase 2: Inject assignment nodes for output vars
|
|
500
|
+
# ──────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
def inject_output_assignments(root: ET.Element, output_var_map: Dict[str, str]) -> int:
|
|
503
|
+
"""
|
|
504
|
+
For each screen that had outputAssignments, inject an <assignments> node
|
|
505
|
+
after the screen to map field output references to the old variable names.
|
|
506
|
+
This preserves compatibility with downstream inputAssignments.
|
|
507
|
+
"""
|
|
508
|
+
if not output_var_map:
|
|
509
|
+
return 0
|
|
510
|
+
|
|
511
|
+
injected = 0
|
|
512
|
+
for screen in findall(root, 'screens'):
|
|
513
|
+
sname = findtext(screen, 'name') or ''
|
|
514
|
+
# Find if any field in this screen had output vars
|
|
515
|
+
screen_fields = {findtext(f, 'name') for f in findall(screen, 'fields')}
|
|
516
|
+
|
|
517
|
+
relevant_vars = {
|
|
518
|
+
old_var: new_ref
|
|
519
|
+
for old_var, new_ref in output_var_map.items()
|
|
520
|
+
if new_ref.split('.')[0] in screen_fields
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if not relevant_vars:
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
# Get current connector target of this screen
|
|
527
|
+
screen_successor = None
|
|
528
|
+
for conn in findall(screen, 'connector'):
|
|
529
|
+
screen_successor = findtext(conn, 'targetReference')
|
|
530
|
+
|
|
531
|
+
# Create assignment node
|
|
532
|
+
assign_name = f'Assign_{sname}_outputs'
|
|
533
|
+
assign_el = make('assignments')
|
|
534
|
+
assign_el.append(make('name', assign_name))
|
|
535
|
+
assign_el.append(make('label', f'Assign {sname} outputs'))
|
|
536
|
+
assign_el.append(make('locationX', '176'))
|
|
537
|
+
assign_el.append(make('locationY', '0'))
|
|
538
|
+
|
|
539
|
+
for old_var, new_ref in relevant_vars.items():
|
|
540
|
+
item = sub(assign_el, 'assignmentItems')
|
|
541
|
+
item.append(make('assignToReference', old_var))
|
|
542
|
+
item.append(make('operator', 'Assign'))
|
|
543
|
+
v = sub(item, 'value')
|
|
544
|
+
v.append(make('elementReference', new_ref))
|
|
545
|
+
|
|
546
|
+
# Connect: screen → assign → screen_successor
|
|
547
|
+
# Update screen connector to point to assignment
|
|
548
|
+
for conn in findall(screen, 'connector'):
|
|
549
|
+
for tr in findall(conn, 'targetReference'):
|
|
550
|
+
tr.text = assign_name
|
|
551
|
+
|
|
552
|
+
# Connect assignment to screen's old successor
|
|
553
|
+
if screen_successor:
|
|
554
|
+
conn_el = sub(assign_el, 'connector')
|
|
555
|
+
conn_el.append(make('targetReference', screen_successor))
|
|
556
|
+
|
|
557
|
+
root.append(assign_el)
|
|
558
|
+
injected += 1
|
|
559
|
+
|
|
560
|
+
return injected
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
# ──────────────────────────────────────────────
|
|
564
|
+
# Phase 3: CUD ordering enforcement
|
|
565
|
+
# ──────────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
def _resolve_fault_path_violations(
|
|
568
|
+
root: ET.Element, is_subflow: bool
|
|
569
|
+
) -> Tuple[int, int, List[str], List[Dict]]:
|
|
570
|
+
"""Sever every CUD→non-CUD fault edge and prune orphaned handler chains.
|
|
571
|
+
|
|
572
|
+
Returns (fixed, skipped, warnings, excision_flags). For a top-level flow
|
|
573
|
+
(is_subflow=False) fault edges are left in place and each is reported as a
|
|
574
|
+
manual-review warning, consistent with the existing warn-only behavior for
|
|
575
|
+
other unrelocatable violations.
|
|
576
|
+
"""
|
|
577
|
+
fixed = 0
|
|
578
|
+
skipped = 0
|
|
579
|
+
warnings: List[str] = []
|
|
580
|
+
flags: List[Dict] = []
|
|
581
|
+
|
|
582
|
+
nodes, _ = build_graph(root)
|
|
583
|
+
severed_targets: Set[str] = set()
|
|
584
|
+
for cud_name, (ntype, cud_el, _targets) in nodes.items():
|
|
585
|
+
if ntype not in CUD_TYPES:
|
|
586
|
+
continue
|
|
587
|
+
for ft in get_fault_targets(cud_el):
|
|
588
|
+
if ft in nodes and nodes[ft][0] not in CUD_TYPES:
|
|
589
|
+
if not is_subflow:
|
|
590
|
+
warnings.append(
|
|
591
|
+
f'CUD "{cud_name}" has a fault path to non-CUD '
|
|
592
|
+
f'[{nodes[ft][0]}] {ft}, which DataCaptureFlow rejects. '
|
|
593
|
+
f'Top-level flow left unchanged — remove or restructure '
|
|
594
|
+
f'the fault handler manually in Flow Builder.'
|
|
595
|
+
)
|
|
596
|
+
skipped += 1
|
|
597
|
+
continue
|
|
598
|
+
if _remove_fault_connector(cud_el, ft):
|
|
599
|
+
severed_targets.add(ft)
|
|
600
|
+
fixed += 1
|
|
601
|
+
|
|
602
|
+
if severed_targets:
|
|
603
|
+
flags.extend(_prune_unreachable_from(root, severed_targets))
|
|
604
|
+
return fixed, skipped, warnings, flags
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def enforce_cud_at_end(root: ET.Element, is_subflow: bool = False) -> Tuple[int, int, List[str], List[Dict]]:
|
|
608
|
+
"""
|
|
609
|
+
Restructure the flow so all CUD operations occur after the last screen.
|
|
610
|
+
Returns (fixed_count, skipped_count, warning_messages, excision_flags).
|
|
611
|
+
|
|
612
|
+
Patterns handled:
|
|
613
|
+
A: CUD → Screen → move screen to before CUD (confirmation pattern)
|
|
614
|
+
B: CUD → Lookup → hoist lookup to before first screen
|
|
615
|
+
C: CUD → Assignment → hoist assignment to before first screen
|
|
616
|
+
|
|
617
|
+
When is_subflow=True, a CUD that none of the above patterns can relocate
|
|
618
|
+
is excised along with its single direct gating Decision (DataCaptureFlow
|
|
619
|
+
subflows cannot contain CUD at all) instead of being left as a warning.
|
|
620
|
+
"""
|
|
621
|
+
fixed = 0
|
|
622
|
+
skipped = 0
|
|
623
|
+
warnings = []
|
|
624
|
+
excision_flags: List[Dict] = []
|
|
625
|
+
max_iterations = 50 # guard against infinite loops
|
|
626
|
+
|
|
627
|
+
# Pre-pass: resolve CUD fault-path violations before the success-path loop.
|
|
628
|
+
# A CUD's fault handler (e.g. Set_FlowError → error screen) can never be made
|
|
629
|
+
# compliant: non-CUD elements are illegal after a CUD on the fault path too,
|
|
630
|
+
# and a fault handler only runs if the CUD failed, so it cannot be hoisted
|
|
631
|
+
# before the CUD the way patterns A-D relocate success-path targets. The
|
|
632
|
+
# hoist patterns also never touch <faultConnector>, so feeding a fault-path
|
|
633
|
+
# violation into them rewrites the wrong (success) edge and thrashes to the
|
|
634
|
+
# iteration cap. Sever fault edges here and prune the orphaned handler chain,
|
|
635
|
+
# leaving the iterative loop below to see a graph with only success edges.
|
|
636
|
+
f_fixed, f_skipped, f_warnings, f_flags = _resolve_fault_path_violations(
|
|
637
|
+
root, is_subflow)
|
|
638
|
+
fixed += f_fixed
|
|
639
|
+
skipped += f_skipped
|
|
640
|
+
warnings.extend(f_warnings)
|
|
641
|
+
excision_flags.extend(f_flags)
|
|
642
|
+
|
|
643
|
+
for _ in range(max_iterations):
|
|
644
|
+
nodes, start_target = build_graph(root)
|
|
645
|
+
order = bfs_order(nodes, start_target)
|
|
646
|
+
violations = find_violations(nodes)
|
|
647
|
+
|
|
648
|
+
if not violations:
|
|
649
|
+
break
|
|
650
|
+
|
|
651
|
+
made_progress = False
|
|
652
|
+
else_branch_hits = [] # (vtype, vname) warned+skipped via the else branch this iteration
|
|
653
|
+
|
|
654
|
+
for vtype, vname in violations:
|
|
655
|
+
nodes, start_target = build_graph(root) # refresh after each fix
|
|
656
|
+
order = bfs_order(nodes, start_target)
|
|
657
|
+
|
|
658
|
+
if vname not in nodes:
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
# Find which CUD node(s) directly point to this violating node
|
|
662
|
+
cud_preds = [
|
|
663
|
+
p for p in predecessors_of(root, vname)
|
|
664
|
+
if p != '__start__' and p in nodes and nodes[p][0] in CUD_TYPES
|
|
665
|
+
]
|
|
666
|
+
|
|
667
|
+
if not cud_preds:
|
|
668
|
+
# Not directly connected from CUD — might be reachable through
|
|
669
|
+
# another violated node; skip for now, will be picked up next iteration
|
|
670
|
+
continue
|
|
671
|
+
|
|
672
|
+
cud_name = cud_preds[0]
|
|
673
|
+
_, cud_el, _ = nodes[cud_name]
|
|
674
|
+
_, violating_el, v_targets = nodes[vname]
|
|
675
|
+
|
|
676
|
+
# ── Pattern A: CUD → Screen ──────────────────────────
|
|
677
|
+
if vtype == 'screens':
|
|
678
|
+
# Move confirmation screen to BEFORE the CUD chain starts.
|
|
679
|
+
# Find the first CUD node in execution order.
|
|
680
|
+
first_cud = next((n for n in order if n in nodes and nodes[n][0] in CUD_TYPES), None)
|
|
681
|
+
if not first_cud:
|
|
682
|
+
skipped += 1
|
|
683
|
+
warnings.append(f'Cannot fix screen {vname}: no first CUD found')
|
|
684
|
+
continue
|
|
685
|
+
|
|
686
|
+
# Find predecessors of first_cud (non-CUD)
|
|
687
|
+
first_cud_preds = [
|
|
688
|
+
p for p in predecessors_of(root, first_cud)
|
|
689
|
+
if p != '__start__' and (p not in nodes or nodes[p][0] not in CUD_TYPES)
|
|
690
|
+
]
|
|
691
|
+
first_cud_is_start = '__start__' in predecessors_of(root, first_cud)
|
|
692
|
+
|
|
693
|
+
# What does the screen currently point to?
|
|
694
|
+
screen_successor = v_targets[0] if v_targets else None
|
|
695
|
+
|
|
696
|
+
# Disconnect CUD from screen
|
|
697
|
+
update_all_connectors(root, vname, screen_successor or '')
|
|
698
|
+
|
|
699
|
+
# Connect screen to the first CUD
|
|
700
|
+
for conn in findall(violating_el, 'connector') + findall(violating_el, 'defaultConnector'):
|
|
701
|
+
for tr in findall(conn, 'targetReference'):
|
|
702
|
+
tr.text = first_cud
|
|
703
|
+
|
|
704
|
+
# Rewire: predecessor of first_cud now points to screen
|
|
705
|
+
if first_cud_is_start:
|
|
706
|
+
set_start_target(root, vname)
|
|
707
|
+
for pred in first_cud_preds:
|
|
708
|
+
if pred in nodes:
|
|
709
|
+
update_all_connectors(root, first_cud, vname)
|
|
710
|
+
# But only for this predecessor, not globally!
|
|
711
|
+
# Use targeted update on the predecessor element
|
|
712
|
+
_, pred_el, _ = nodes[pred]
|
|
713
|
+
# Undo global update, do targeted
|
|
714
|
+
update_all_connectors(root, vname, first_cud)
|
|
715
|
+
# Now do targeted
|
|
716
|
+
_replace_target_in_node(pred_el, first_cud, vname)
|
|
717
|
+
|
|
718
|
+
fixed += 1
|
|
719
|
+
made_progress = True
|
|
720
|
+
break # restart iteration
|
|
721
|
+
|
|
722
|
+
# ── Pattern B: CUD → Lookup ──────────────────────────
|
|
723
|
+
elif vtype == 'recordLookups':
|
|
724
|
+
# Hoist the lookup to execute before the first screen.
|
|
725
|
+
# Find the first screen in execution order.
|
|
726
|
+
first_screen = next(
|
|
727
|
+
(n for n in order if n in nodes and nodes[n][0] == 'screens'), None
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
# What does the lookup currently point to?
|
|
731
|
+
lookup_successor = v_targets[0] if v_targets else None
|
|
732
|
+
|
|
733
|
+
# Disconnect CUD → Lookup; CUD now points to what lookup pointed to
|
|
734
|
+
update_all_connectors(root, vname, lookup_successor or '')
|
|
735
|
+
|
|
736
|
+
# Insert lookup before first screen (or at start if no screens)
|
|
737
|
+
if first_screen:
|
|
738
|
+
first_screen_preds = [
|
|
739
|
+
p for p in predecessors_of(root, first_screen)
|
|
740
|
+
if p != vname # don't include ourselves
|
|
741
|
+
]
|
|
742
|
+
if '__start__' in first_screen_preds:
|
|
743
|
+
set_start_target(root, vname)
|
|
744
|
+
# Lookup → first_screen
|
|
745
|
+
_set_first_connector(violating_el, first_screen)
|
|
746
|
+
else:
|
|
747
|
+
for pred in first_screen_preds:
|
|
748
|
+
if pred and pred in nodes:
|
|
749
|
+
_replace_target_in_node(nodes[pred][1], first_screen, vname)
|
|
750
|
+
break
|
|
751
|
+
_set_first_connector(violating_el, first_screen)
|
|
752
|
+
else:
|
|
753
|
+
# No screens — insert at start
|
|
754
|
+
old_start = get_start_target(root)
|
|
755
|
+
set_start_target(root, vname)
|
|
756
|
+
_set_first_connector(violating_el, old_start or '')
|
|
757
|
+
|
|
758
|
+
fixed += 1
|
|
759
|
+
made_progress = True
|
|
760
|
+
break # restart iteration
|
|
761
|
+
|
|
762
|
+
# ── Pattern C: CUD → Assignment ──────────────────────
|
|
763
|
+
elif vtype == 'assignments':
|
|
764
|
+
# Same as Pattern B — hoist before first screen
|
|
765
|
+
first_screen = next(
|
|
766
|
+
(n for n in order if n in nodes and nodes[n][0] == 'screens'), None
|
|
767
|
+
)
|
|
768
|
+
assign_successor = v_targets[0] if v_targets else None
|
|
769
|
+
update_all_connectors(root, vname, assign_successor or '')
|
|
770
|
+
|
|
771
|
+
if first_screen:
|
|
772
|
+
first_screen_preds = [
|
|
773
|
+
p for p in predecessors_of(root, first_screen)
|
|
774
|
+
if p != vname
|
|
775
|
+
]
|
|
776
|
+
if '__start__' in first_screen_preds:
|
|
777
|
+
set_start_target(root, vname)
|
|
778
|
+
_set_first_connector(violating_el, first_screen)
|
|
779
|
+
else:
|
|
780
|
+
for pred in first_screen_preds:
|
|
781
|
+
if pred and pred in nodes:
|
|
782
|
+
_replace_target_in_node(nodes[pred][1], first_screen, vname)
|
|
783
|
+
break
|
|
784
|
+
_set_first_connector(violating_el, first_screen)
|
|
785
|
+
else:
|
|
786
|
+
old_start = get_start_target(root)
|
|
787
|
+
set_start_target(root, vname)
|
|
788
|
+
_set_first_connector(violating_el, old_start or '')
|
|
789
|
+
|
|
790
|
+
fixed += 1
|
|
791
|
+
made_progress = True
|
|
792
|
+
break
|
|
793
|
+
|
|
794
|
+
# ── Pattern D: CUD → Decision (complex) ──────────────
|
|
795
|
+
elif vtype == 'decisions':
|
|
796
|
+
# A decision after CUD is allowed ONLY if it only has CUD branches.
|
|
797
|
+
# If it has mixed branches, we need to move it before CUD.
|
|
798
|
+
# For safety, hoist before first screen.
|
|
799
|
+
first_screen = next(
|
|
800
|
+
(n for n in order if n in nodes and nodes[n][0] == 'screens'), None
|
|
801
|
+
)
|
|
802
|
+
dec_successor = v_targets[0] if v_targets else None
|
|
803
|
+
update_all_connectors(root, vname, dec_successor or '')
|
|
804
|
+
|
|
805
|
+
if first_screen:
|
|
806
|
+
first_screen_preds = [p for p in predecessors_of(root, first_screen)
|
|
807
|
+
if p != vname]
|
|
808
|
+
if '__start__' in first_screen_preds:
|
|
809
|
+
set_start_target(root, vname)
|
|
810
|
+
# Decision's default connector → first_screen
|
|
811
|
+
for conn in findall(violating_el, 'defaultConnector'):
|
|
812
|
+
for tr in findall(conn, 'targetReference'):
|
|
813
|
+
tr.text = first_screen
|
|
814
|
+
else:
|
|
815
|
+
for pred in first_screen_preds:
|
|
816
|
+
if pred and pred in nodes:
|
|
817
|
+
_replace_target_in_node(nodes[pred][1], first_screen, vname)
|
|
818
|
+
break
|
|
819
|
+
for conn in findall(violating_el, 'defaultConnector'):
|
|
820
|
+
for tr in findall(conn, 'targetReference'):
|
|
821
|
+
tr.text = first_screen
|
|
822
|
+
|
|
823
|
+
fixed += 1
|
|
824
|
+
made_progress = True
|
|
825
|
+
break
|
|
826
|
+
|
|
827
|
+
else:
|
|
828
|
+
msg = (
|
|
829
|
+
f'Cannot auto-fix [{vtype}] {vname} after CUD. '
|
|
830
|
+
f'Review and fix manually in Flow Builder.'
|
|
831
|
+
)
|
|
832
|
+
warnings.append(msg)
|
|
833
|
+
skipped += 1
|
|
834
|
+
else_branch_hits.append((vtype, vname, msg))
|
|
835
|
+
|
|
836
|
+
if not made_progress:
|
|
837
|
+
# Stuck — report remaining violations
|
|
838
|
+
nodes, start_target = build_graph(root)
|
|
839
|
+
order = bfs_order(nodes, start_target)
|
|
840
|
+
remaining = find_violations(nodes)
|
|
841
|
+
if is_subflow:
|
|
842
|
+
handled_cuds = set()
|
|
843
|
+
for vt, vn in remaining:
|
|
844
|
+
cud_preds = [
|
|
845
|
+
p for p in predecessors_of(root, vn)
|
|
846
|
+
if p != '__start__' and p in nodes and nodes[p][0] in CUD_TYPES
|
|
847
|
+
]
|
|
848
|
+
for cud_name in cud_preds:
|
|
849
|
+
if cud_name in handled_cuds:
|
|
850
|
+
continue
|
|
851
|
+
handled_cuds.add(cud_name)
|
|
852
|
+
gate_preds = [
|
|
853
|
+
p for p in predecessors_of(root, cud_name)
|
|
854
|
+
if p != '__start__' and p in nodes and nodes[p][0] == 'decisions'
|
|
855
|
+
]
|
|
856
|
+
if len(gate_preds) > 1:
|
|
857
|
+
warnings.append(
|
|
858
|
+
f'CUD "{cud_name}" has multiple gating Decisions '
|
|
859
|
+
f'({", ".join(gate_preds)}); only "{gate_preds[0]}" was '
|
|
860
|
+
f'excised with it — review the others manually in Flow Builder.'
|
|
861
|
+
)
|
|
862
|
+
group = ([gate_preds[0], cud_name] if gate_preds else [cud_name])
|
|
863
|
+
excision_flags.extend(
|
|
864
|
+
excise_and_placeholder(
|
|
865
|
+
root, group, 'CUD_EXCISED_IN_SUBFLOW',
|
|
866
|
+
f'CUD "{cud_name}" could not be relocated to the end of '
|
|
867
|
+
f'the subflow by any of the four hoist patterns; DataCaptureFlow '
|
|
868
|
+
f'subflows cannot contain CUD, so it was excised with its '
|
|
869
|
+
f'gating Decision.'))
|
|
870
|
+
# Re-check: excising may have created fresh non-CUD-only successors that
|
|
871
|
+
# are now compliant (the CUD is gone). Recompute once more; anything still
|
|
872
|
+
# left is a genuinely different, non-CUD violation shape — warn as before.
|
|
873
|
+
nodes, start_target = build_graph(root)
|
|
874
|
+
order = bfs_order(nodes, start_target)
|
|
875
|
+
remaining = find_violations(nodes)
|
|
876
|
+
# The main loop's else branch (above) may have already warned+skipped
|
|
877
|
+
# some of these same violations earlier in THIS iteration, before we
|
|
878
|
+
# knew excision would resolve them. Strip those now-stale entries so
|
|
879
|
+
# a resolved violation doesn't surface both a warning and a flag.
|
|
880
|
+
remaining_set = set(remaining)
|
|
881
|
+
for vt, vn, msg in else_branch_hits:
|
|
882
|
+
if (vt, vn) not in remaining_set:
|
|
883
|
+
if msg in warnings:
|
|
884
|
+
warnings.remove(msg)
|
|
885
|
+
skipped -= 1
|
|
886
|
+
for vt, vn in remaining:
|
|
887
|
+
warnings.append(
|
|
888
|
+
f'Could not auto-fix [{vt}] {vn} after CUD — '
|
|
889
|
+
f'complex dependency. Fix manually in Flow Builder.'
|
|
890
|
+
)
|
|
891
|
+
skipped += 1
|
|
892
|
+
break
|
|
893
|
+
|
|
894
|
+
# Unconditional CUD ban for subflows. The ordering loop above only excises
|
|
895
|
+
# CUD that is implicated in an ordering violation, but a DataCaptureFlow used
|
|
896
|
+
# as a subflow may contain NO CUD at all — even a self-ordered CUD chain
|
|
897
|
+
# (e.g. Update → Create → terminal) is rejected by the calling flow with
|
|
898
|
+
# "You can't use Create, Update, or Delete elements in a Data Capture flow
|
|
899
|
+
# subflow." So after the ordering pass has done its work, sweep every CUD
|
|
900
|
+
# node still present and excise it too. Runs after _resolve_fault_path_
|
|
901
|
+
# violations and the main loop, so it operates on the final, fault-cleaned
|
|
902
|
+
# graph.
|
|
903
|
+
if is_subflow:
|
|
904
|
+
for _ in range(max_iterations):
|
|
905
|
+
nodes, _ = build_graph(root)
|
|
906
|
+
remaining_cuds = [n for n, (nt, _e, _t) in nodes.items() if nt in CUD_TYPES]
|
|
907
|
+
if not remaining_cuds:
|
|
908
|
+
break
|
|
909
|
+
cud_name = remaining_cuds[0]
|
|
910
|
+
gate_preds = [
|
|
911
|
+
p for p in predecessors_of(root, cud_name)
|
|
912
|
+
if p != '__start__' and p in nodes and nodes[p][0] == 'decisions'
|
|
913
|
+
]
|
|
914
|
+
if len(gate_preds) > 1:
|
|
915
|
+
warnings.append(
|
|
916
|
+
f'CUD "{cud_name}" has multiple gating Decisions '
|
|
917
|
+
f'({", ".join(gate_preds)}); only "{gate_preds[0]}" was '
|
|
918
|
+
f'excised with it — review the others manually in Flow Builder.'
|
|
919
|
+
)
|
|
920
|
+
group = ([gate_preds[0], cud_name] if gate_preds else [cud_name])
|
|
921
|
+
excision_flags.extend(
|
|
922
|
+
excise_and_placeholder(
|
|
923
|
+
root, group, 'CUD_EXCISED_IN_SUBFLOW',
|
|
924
|
+
f'CUD "{cud_name}" was excised because DataCaptureFlow subflows '
|
|
925
|
+
f'cannot contain any Create, Update, or Delete elements'
|
|
926
|
+
+ (f'; its gating Decision "{gate_preds[0]}" was excised with it.'
|
|
927
|
+
if gate_preds else '.')))
|
|
928
|
+
|
|
929
|
+
return fixed, skipped, warnings, excision_flags
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _replace_target_in_node(node_el: ET.Element, old: str, new: str):
|
|
933
|
+
"""Replace old target with new in all connectors of a single node."""
|
|
934
|
+
for conn in (findall(node_el, 'connector') +
|
|
935
|
+
findall(node_el, 'defaultConnector') +
|
|
936
|
+
findall(node_el, 'nextValueConnector') +
|
|
937
|
+
findall(node_el, 'noMoreValuesConnector') +
|
|
938
|
+
findall(node_el, 'faultConnector')):
|
|
939
|
+
for tr in findall(conn, 'targetReference'):
|
|
940
|
+
if tr.text == old:
|
|
941
|
+
tr.text = new
|
|
942
|
+
for rule in findall(node_el, 'rules'):
|
|
943
|
+
for conn in findall(rule, 'connector'):
|
|
944
|
+
for tr in findall(conn, 'targetReference'):
|
|
945
|
+
if tr.text == old:
|
|
946
|
+
tr.text = new
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _remove_fault_connector(node_el: ET.Element, target: str) -> bool:
|
|
950
|
+
"""Delete the <faultConnector> on node_el whose targetReference is `target`.
|
|
951
|
+
|
|
952
|
+
Success connectors are left untouched — this is deliberately fault-only so a
|
|
953
|
+
CUD's illegal fault edge can be severed without disturbing its success path.
|
|
954
|
+
"""
|
|
955
|
+
removed = False
|
|
956
|
+
for conn in findall(node_el, 'faultConnector'):
|
|
957
|
+
if findtext(conn, 'targetReference') == target:
|
|
958
|
+
node_el.remove(conn)
|
|
959
|
+
removed = True
|
|
960
|
+
return removed
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _prune_unreachable_from(root: ET.Element, candidate_roots: Set[str]) -> List[Dict]:
|
|
964
|
+
"""Remove nodes that became unreachable after fault edges were severed.
|
|
965
|
+
|
|
966
|
+
Only descendants of `candidate_roots` (the severed fault targets) that are no
|
|
967
|
+
longer reachable from start are removed, so pre-existing dead code is left
|
|
968
|
+
alone. Returns one manual-review flag per removed node.
|
|
969
|
+
"""
|
|
970
|
+
nodes, start = build_graph(root)
|
|
971
|
+
reachable = set(bfs_order(nodes, start))
|
|
972
|
+
|
|
973
|
+
to_remove: List[str] = []
|
|
974
|
+
seen: Set[str] = set()
|
|
975
|
+
stack = [r for r in candidate_roots if r in nodes and r not in reachable]
|
|
976
|
+
while stack:
|
|
977
|
+
n = stack.pop()
|
|
978
|
+
if n in seen or n not in nodes or n in reachable:
|
|
979
|
+
continue
|
|
980
|
+
seen.add(n)
|
|
981
|
+
to_remove.append(n)
|
|
982
|
+
for t in nodes[n][2]:
|
|
983
|
+
if t not in reachable and t not in seen:
|
|
984
|
+
stack.append(t)
|
|
985
|
+
|
|
986
|
+
flags = []
|
|
987
|
+
for n in to_remove:
|
|
988
|
+
_, el, _ = nodes[n]
|
|
989
|
+
root.remove(el)
|
|
990
|
+
flags.append(build_excision_flag(
|
|
991
|
+
n, 'CUD_FAULT_PATH_EXCISED',
|
|
992
|
+
f'"{n}" was only reachable as a fault handler downstream of a CUD. '
|
|
993
|
+
f'DataCaptureFlow does not allow non-CUD elements after a CUD on any '
|
|
994
|
+
f'path — including the fault path — and a fault handler cannot be '
|
|
995
|
+
f'hoisted before the CUD, so it was removed.'))
|
|
996
|
+
return flags
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _set_first_connector(node_el: ET.Element, target: str):
|
|
1000
|
+
"""Set/update the first <connector> targetReference on a node."""
|
|
1001
|
+
for conn in findall(node_el, 'connector'):
|
|
1002
|
+
for tr in findall(conn, 'targetReference'):
|
|
1003
|
+
tr.text = target
|
|
1004
|
+
return
|
|
1005
|
+
# No connector exists — create one
|
|
1006
|
+
conn = sub(node_el, 'connector')
|
|
1007
|
+
conn.append(make('targetReference', target))
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def excise_and_placeholder(root: ET.Element, node_names: List[str], code: str,
|
|
1011
|
+
why: str) -> List[Dict]:
|
|
1012
|
+
"""Excise an ordered group of nodes, insert one placeholder Screen per
|
|
1013
|
+
excised node (chained in order), rewire predecessor -> placeholders ->
|
|
1014
|
+
original downstream, and return one manual-review flag dict per node.
|
|
1015
|
+
|
|
1016
|
+
If the flow's start target points directly at ANY name in node_names, the
|
|
1017
|
+
start target is redirected to the first placeholder instead of (or in
|
|
1018
|
+
addition to) rewiring predecessor elements.
|
|
1019
|
+
|
|
1020
|
+
Predecessors are resolved for the WHOLE group, not just node_names[0]:
|
|
1021
|
+
some other node in the flow may point directly at a later member of the
|
|
1022
|
+
group (e.g. straight at the CUD, bypassing its gating Decision). Every
|
|
1023
|
+
such external predecessor (excluding predecessors that are themselves
|
|
1024
|
+
members of the group — those are internal edges removed along with the
|
|
1025
|
+
group) gets every group-member reference it holds redirected to the
|
|
1026
|
+
first placeholder, since the whole group collapses down to the
|
|
1027
|
+
placeholder chain that starts there. Skipping this for multi-member
|
|
1028
|
+
groups leaves a dangling <targetReference> once the group's elements are
|
|
1029
|
+
removed below.
|
|
1030
|
+
|
|
1031
|
+
node_names: ordered API names to excise as one group (CUD case:
|
|
1032
|
+
[decision_name, cud_name]; the other two excision sites pass a single-
|
|
1033
|
+
element list).
|
|
1034
|
+
"""
|
|
1035
|
+
nodes, _ = build_graph(root)
|
|
1036
|
+
name_set = set(node_names)
|
|
1037
|
+
|
|
1038
|
+
preds_by_name = {n: predecessors_of(root, n) for n in node_names}
|
|
1039
|
+
from_start = any('__start__' in preds_by_name[n] for n in node_names)
|
|
1040
|
+
pred_names = {
|
|
1041
|
+
p for n in node_names for p in preds_by_name[n]
|
|
1042
|
+
if p != '__start__' and p not in name_set and p in nodes
|
|
1043
|
+
}
|
|
1044
|
+
pred_elements = [nodes[p][1] for p in pred_names]
|
|
1045
|
+
|
|
1046
|
+
_, last_el, _ = nodes[node_names[-1]]
|
|
1047
|
+
downstream_targets = get_connector_targets(last_el)
|
|
1048
|
+
downstream = downstream_targets[0] if downstream_targets else None
|
|
1049
|
+
|
|
1050
|
+
elements_to_remove = [nodes[n][1] for n in node_names]
|
|
1051
|
+
orig_locations = {
|
|
1052
|
+
n: (findtext(el, 'locationX'), findtext(el, 'locationY'))
|
|
1053
|
+
for n, el in zip(node_names, elements_to_remove)
|
|
1054
|
+
}
|
|
1055
|
+
for el in elements_to_remove:
|
|
1056
|
+
root.remove(el)
|
|
1057
|
+
|
|
1058
|
+
placeholder_names = [f'{n}_Excised_Placeholder' for n in node_names]
|
|
1059
|
+
placeholders = []
|
|
1060
|
+
for name, orig_name in zip(placeholder_names, node_names):
|
|
1061
|
+
loc_x, loc_y = orig_locations[orig_name]
|
|
1062
|
+
screen = make('screens')
|
|
1063
|
+
screen.append(make('name', name))
|
|
1064
|
+
screen.append(make('label', name))
|
|
1065
|
+
screen.append(make('locationX', loc_x if loc_x is not None else '176'))
|
|
1066
|
+
screen.append(make('locationY', loc_y if loc_y is not None else '0'))
|
|
1067
|
+
field = sub(screen, 'fields')
|
|
1068
|
+
field.append(make('name', f'{name}_Field'))
|
|
1069
|
+
field.append(make('fieldType', 'DisplayText'))
|
|
1070
|
+
field.append(make('fieldText',
|
|
1071
|
+
f"ATTENTION: The original {orig_name} was dropped since it is not "
|
|
1072
|
+
f"compliant with data capture. More info in the migration summary."))
|
|
1073
|
+
screen.append(make('allowFinish', 'true'))
|
|
1074
|
+
screen.append(make('showFooter', 'true'))
|
|
1075
|
+
screen.append(make('nextOrFinishButtonLabel', 'Next'))
|
|
1076
|
+
placeholders.append(screen)
|
|
1077
|
+
|
|
1078
|
+
for i in range(len(placeholders) - 1):
|
|
1079
|
+
conn = sub(placeholders[i], 'connector')
|
|
1080
|
+
conn.append(make('targetReference', placeholder_names[i + 1]))
|
|
1081
|
+
if downstream:
|
|
1082
|
+
conn = sub(placeholders[-1], 'connector')
|
|
1083
|
+
conn.append(make('targetReference', downstream))
|
|
1084
|
+
|
|
1085
|
+
for screen in placeholders:
|
|
1086
|
+
root.append(screen)
|
|
1087
|
+
|
|
1088
|
+
if from_start:
|
|
1089
|
+
set_start_target(root, placeholder_names[0])
|
|
1090
|
+
for pred_el in pred_elements:
|
|
1091
|
+
for n in node_names:
|
|
1092
|
+
_replace_target_in_node(pred_el, n, placeholder_names[0])
|
|
1093
|
+
|
|
1094
|
+
return [build_excision_flag(n, code, why) for n in node_names]
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
# ──────────────────────────────────────────────
|
|
1098
|
+
# Phase 4: DC structural requirements
|
|
1099
|
+
# ──────────────────────────────────────────────
|
|
1100
|
+
|
|
1101
|
+
# Builder-selection processMetadataValues that steer Flow Builder to the modern
|
|
1102
|
+
# auto-layout Lightning canvas. Values match build_flow.py (native build path).
|
|
1103
|
+
BUILDER_METADATA = (
|
|
1104
|
+
('BuilderType', 'LightningFlowBuilder'),
|
|
1105
|
+
('OriginBuilderType', 'LightningFlowBuilder'),
|
|
1106
|
+
('CanvasMode', 'AUTO_LAYOUT_CANVAS'),
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def set_builder_metadata(root: ET.Element):
|
|
1111
|
+
"""Ensure BuilderType/OriginBuilderType/CanvasMode processMetadataValues are
|
|
1112
|
+
present with the auto-layout Lightning values.
|
|
1113
|
+
|
|
1114
|
+
Adds missing entries; overwrites an existing entry's stringValue (e.g. FSM's
|
|
1115
|
+
CanvasMode=FREE_FORM_CANVAS) so the migrated flow opens in the same builder as a
|
|
1116
|
+
natively-built DataCaptureFlow. Other processMetadataValues are left untouched.
|
|
1117
|
+
"""
|
|
1118
|
+
existing = {findtext(p, 'name'): p for p in findall(root, 'processMetadataValues')}
|
|
1119
|
+
for name, string_value in BUILDER_METADATA:
|
|
1120
|
+
pmv = existing.get(name)
|
|
1121
|
+
if pmv is None:
|
|
1122
|
+
pmv = make('processMetadataValues')
|
|
1123
|
+
sub(pmv, 'name', name)
|
|
1124
|
+
value = sub(pmv, 'value')
|
|
1125
|
+
sub(value, 'stringValue', string_value)
|
|
1126
|
+
root.append(pmv)
|
|
1127
|
+
else:
|
|
1128
|
+
value_els = findall(pmv, 'value')
|
|
1129
|
+
value = value_els[0] if value_els else sub(pmv, 'value')
|
|
1130
|
+
sv_els = findall(value, 'stringValue')
|
|
1131
|
+
if sv_els:
|
|
1132
|
+
sv_els[0].text = string_value
|
|
1133
|
+
else:
|
|
1134
|
+
sub(value, 'stringValue', string_value)
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def apply_dc_metadata(root: ET.Element):
|
|
1138
|
+
"""Change processType, fix environments, remove invalid metadata."""
|
|
1139
|
+
# processType
|
|
1140
|
+
for pt in findall(root, 'processType'):
|
|
1141
|
+
pt.text = 'DataCaptureFlow'
|
|
1142
|
+
|
|
1143
|
+
# Remove apiVersion
|
|
1144
|
+
for av in findall(root, 'apiVersion'):
|
|
1145
|
+
root.remove(av)
|
|
1146
|
+
|
|
1147
|
+
# areMetricsLoggedToDataCloud
|
|
1148
|
+
for el in findall(root, 'areMetricsLoggedToDataCloud'):
|
|
1149
|
+
root.remove(el)
|
|
1150
|
+
|
|
1151
|
+
# Force status to Draft: a migrated flow must deploy INACTIVE so it is
|
|
1152
|
+
# validated in Flow Builder and device-tested before going live (skill
|
|
1153
|
+
# Step 8). The source flow's <status>Active</status> must not carry over,
|
|
1154
|
+
# or the deploy silently activates a never-tested flow.
|
|
1155
|
+
status_els = findall(root, 'status')
|
|
1156
|
+
if status_els:
|
|
1157
|
+
for st in status_els:
|
|
1158
|
+
st.text = 'Draft'
|
|
1159
|
+
else:
|
|
1160
|
+
root.append(make('status', 'Draft'))
|
|
1161
|
+
|
|
1162
|
+
# environments: replace Default with Offline, or add if missing
|
|
1163
|
+
envs = findall(root, 'environments')
|
|
1164
|
+
if envs:
|
|
1165
|
+
for env in envs:
|
|
1166
|
+
if env.text in (None, 'Default', ''):
|
|
1167
|
+
env.text = 'Offline'
|
|
1168
|
+
else:
|
|
1169
|
+
root.append(make('environments', 'Offline'))
|
|
1170
|
+
|
|
1171
|
+
# Remove Constants (not allowed in DataCaptureFlow)
|
|
1172
|
+
for el in findall(root, 'constants'):
|
|
1173
|
+
root.remove(el)
|
|
1174
|
+
|
|
1175
|
+
# Builder-selection metadata. Flow Builder chooses its canvas/chrome from these
|
|
1176
|
+
# processMetadataValues, NOT from processType — a DataCaptureFlow lacking them (or
|
|
1177
|
+
# carrying FSM's CanvasMode=FREE_FORM_CANVAS) opens in the legacy free-form builder
|
|
1178
|
+
# instead of the modern auto-layout Lightning builder that a natively-built DC flow
|
|
1179
|
+
# gets. Force the auto-layout values so migrated flows open the same way.
|
|
1180
|
+
set_builder_metadata(root)
|
|
1181
|
+
|
|
1182
|
+
# status: always emit Draft. The source flow's status (often Active) must
|
|
1183
|
+
# NOT leak through — a migrated flow is unproven until its dry-run passes and
|
|
1184
|
+
# it has been tested on device. deploy_flow.sh also force-rewrites <status>
|
|
1185
|
+
# to Draft at the deploy boundary, so this holds even for a hand-edited file.
|
|
1186
|
+
# Activation is a manual admin action in the org UI (SKILL.md Step 8), never
|
|
1187
|
+
# scripted or inherited from the source.
|
|
1188
|
+
status_els = findall(root, 'status')
|
|
1189
|
+
if status_els:
|
|
1190
|
+
for el in status_els:
|
|
1191
|
+
el.text = 'Draft'
|
|
1192
|
+
else:
|
|
1193
|
+
root.append(make('status', 'Draft'))
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def ensure_screen_attrs(root: ET.Element):
|
|
1197
|
+
"""Add required DataCaptureFlow screen attributes if missing."""
|
|
1198
|
+
for screen in findall(root, 'screens'):
|
|
1199
|
+
for attr, val in [('allowFinish', 'true'),
|
|
1200
|
+
('showFooter', 'true'),
|
|
1201
|
+
('nextOrFinishButtonLabel', 'Next')]:
|
|
1202
|
+
if findtext(screen, attr) is None:
|
|
1203
|
+
screen.append(make(attr, val))
|
|
1204
|
+
|
|
1205
|
+
# DC's runtime uses allowFinish as the footer-render gate, not as a
|
|
1206
|
+
# "this is the last screen" marker like classic Screen Flow — a
|
|
1207
|
+
# legacy FieldServiceMobile screen correctly carrying
|
|
1208
|
+
# allowFinish=false (non-terminal, has an outgoing connector) leaves
|
|
1209
|
+
# the DC screen with no footer/Next button at all, blocking
|
|
1210
|
+
# navigation. Navigation still follows the connector graph, so this
|
|
1211
|
+
# is safe to force true on every screen regardless of position.
|
|
1212
|
+
for el in findall(screen, 'allowFinish'):
|
|
1213
|
+
el.text = 'true'
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
# Canvas node element types the Metadata API requires to carry locationX/locationY.
|
|
1217
|
+
CANVAS_NODE_TYPES = frozenset((
|
|
1218
|
+
'screens', 'decisions', 'recordLookups', 'recordCreates', 'recordUpdates',
|
|
1219
|
+
'recordDeletes', 'loops', 'assignments', 'subflows', 'actionCalls', 'waits',
|
|
1220
|
+
'steps', 'apexPluginCalls', 'collectionProcessors', 'customErrors',
|
|
1221
|
+
'recordRollbacks', 'transforms', 'orchestratedStages',
|
|
1222
|
+
))
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def ensure_node_locations(root: ET.Element):
|
|
1226
|
+
"""Every canvas node element must carry locationX/locationY to deploy.
|
|
1227
|
+
|
|
1228
|
+
Auto-layout ignores the coordinates at runtime, but the Metadata API schema
|
|
1229
|
+
still requires them present: a source flow authored (or retrieved) without
|
|
1230
|
+
canvas coords on a screen would otherwise produce a migrated flow that fails
|
|
1231
|
+
to deploy with "Required field is missing: locationX". Only excised
|
|
1232
|
+
placeholder screens got a coordinate fallback before; pass-through nodes kept
|
|
1233
|
+
whatever the source had (possibly nothing). Insert both right after <label>
|
|
1234
|
+
(or <name>), matching the schema element order, preserving any existing pair.
|
|
1235
|
+
"""
|
|
1236
|
+
for el in list(root):
|
|
1237
|
+
if local(el) not in CANVAS_NODE_TYPES:
|
|
1238
|
+
continue
|
|
1239
|
+
x, y = el.find(tag('locationX')), el.find(tag('locationY'))
|
|
1240
|
+
if x is not None and y is not None:
|
|
1241
|
+
continue # already positioned — leave real coords untouched
|
|
1242
|
+
xv = x.text if x is not None else '176'
|
|
1243
|
+
yv = y.text if y is not None else '0'
|
|
1244
|
+
for existing in (x, y):
|
|
1245
|
+
if existing is not None:
|
|
1246
|
+
el.remove(existing)
|
|
1247
|
+
pos = 0
|
|
1248
|
+
for i, child in enumerate(list(el)):
|
|
1249
|
+
if local(child) in ('name', 'label'):
|
|
1250
|
+
pos = i + 1
|
|
1251
|
+
el.insert(pos, make('locationY', yv))
|
|
1252
|
+
el.insert(pos, make('locationX', xv))
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
DC_INPUT_VARS = [
|
|
1256
|
+
('recordId', 'String'),
|
|
1257
|
+
('parentRecordId', 'String'),
|
|
1258
|
+
('parentObjectType', 'String'),
|
|
1259
|
+
]
|
|
1260
|
+
|
|
1261
|
+
def ensure_dc_variables(root: ET.Element):
|
|
1262
|
+
"""
|
|
1263
|
+
Ensure required DC input variables exist with correct attributes.
|
|
1264
|
+
Uses case-insensitive dedup — if a var with the same name (any case) exists,
|
|
1265
|
+
update it to be isInput=true rather than adding a duplicate.
|
|
1266
|
+
"""
|
|
1267
|
+
all_vars = findall(root, 'variables')
|
|
1268
|
+
var_by_lower = {(findtext(v, 'name') or '').lower(): v for v in all_vars}
|
|
1269
|
+
|
|
1270
|
+
for var_name, data_type in DC_INPUT_VARS:
|
|
1271
|
+
existing = var_by_lower.get(var_name.lower())
|
|
1272
|
+
if existing is not None:
|
|
1273
|
+
# Ensure isInput=true
|
|
1274
|
+
isInput_elements = findall(existing, 'isInput')
|
|
1275
|
+
if isInput_elements:
|
|
1276
|
+
# Child exists - update it to 'true'
|
|
1277
|
+
for el in isInput_elements:
|
|
1278
|
+
if el.text != 'true':
|
|
1279
|
+
el.text = 'true'
|
|
1280
|
+
else:
|
|
1281
|
+
# Child missing - add it with 'true'
|
|
1282
|
+
existing.append(make('isInput', 'true'))
|
|
1283
|
+
else:
|
|
1284
|
+
var_el = make('variables')
|
|
1285
|
+
var_el.append(make('name', var_name))
|
|
1286
|
+
var_el.append(make('dataType', data_type))
|
|
1287
|
+
var_el.append(make('isCollection', 'false'))
|
|
1288
|
+
var_el.append(make('isInput', 'true'))
|
|
1289
|
+
var_el.append(make('isOutput', 'false'))
|
|
1290
|
+
root.append(var_el)
|
|
1291
|
+
|
|
1292
|
+
# Regroup variables to be contiguous (Salesforce schema requirement)
|
|
1293
|
+
all_vars_refreshed = findall(root, 'variables')
|
|
1294
|
+
seen_names: Set[str] = set()
|
|
1295
|
+
unique_vars = []
|
|
1296
|
+
for v in all_vars_refreshed:
|
|
1297
|
+
vname = (findtext(v, 'name') or '').lower()
|
|
1298
|
+
if vname not in seen_names:
|
|
1299
|
+
seen_names.add(vname)
|
|
1300
|
+
unique_vars.append(v)
|
|
1301
|
+
for v in all_vars_refreshed:
|
|
1302
|
+
try:
|
|
1303
|
+
root.remove(v)
|
|
1304
|
+
except ValueError:
|
|
1305
|
+
pass
|
|
1306
|
+
for v in unique_vars:
|
|
1307
|
+
root.append(v)
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def _field_is_multiselect(field: ET.Element) -> bool:
|
|
1311
|
+
"""True if a dcLookup field carries isMultiSelection=true."""
|
|
1312
|
+
for ip in findall(field, 'inputParameters'):
|
|
1313
|
+
if findtext(ip, 'name') == 'isMultiSelection':
|
|
1314
|
+
for val in findall(ip, 'value'):
|
|
1315
|
+
if (findtext(val, 'booleanValue') or '').lower() == 'true':
|
|
1316
|
+
return True
|
|
1317
|
+
return False
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
def _accessor_suffix(field: ET.Element) -> Optional[str]:
|
|
1321
|
+
"""Return the DC accessor suffix for a screen field, or None when it can't
|
|
1322
|
+
be resolved from the field alone (unknown/unconverted type, or dcName which
|
|
1323
|
+
has two accessors — .firstName / .lastName — the caller must choose).
|
|
1324
|
+
|
|
1325
|
+
Mirrors the accessor table in SKILL.md (## Visibility rule accessor suffixes):
|
|
1326
|
+
choice (dcPicklist/dcRbGroup/dcCbGroup) -> selectedChoiceValues
|
|
1327
|
+
dcToggle -> isActive
|
|
1328
|
+
dcLookup single -> recordId
|
|
1329
|
+
dcLookup multi (isMultiSelection=true) -> recordIds
|
|
1330
|
+
dcName -> None (ambiguous — warn)
|
|
1331
|
+
other ComponentInstance -> value
|
|
1332
|
+
"""
|
|
1333
|
+
ft = findtext(field, 'fieldType') or ''
|
|
1334
|
+
ext = findtext(field, 'extensionName') or ''
|
|
1335
|
+
if ft in ('ComponentChoice', 'ComponentMultiChoice'):
|
|
1336
|
+
return 'selectedChoiceValues'
|
|
1337
|
+
if ft == 'ComponentInstance':
|
|
1338
|
+
if ext.endswith('dcToggle'):
|
|
1339
|
+
return 'isActive'
|
|
1340
|
+
if ext.endswith('dcLookup'):
|
|
1341
|
+
return 'recordIds' if _field_is_multiselect(field) else 'recordId'
|
|
1342
|
+
if ext.endswith('dcName'):
|
|
1343
|
+
return None # ambiguous — .firstName / .lastName
|
|
1344
|
+
return 'value'
|
|
1345
|
+
return None # unknown / unconverted field type
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def fix_screen_field_references(root: ET.Element) -> List[str]:
|
|
1349
|
+
"""
|
|
1350
|
+
In DataCaptureFlow, screen fields are accessed via a typed accessor suffix
|
|
1351
|
+
(not the bare field name). Fix all downstream references in recordCreates,
|
|
1352
|
+
recordUpdates, decisions, assignments, and loops. The suffix per component
|
|
1353
|
+
type is resolved by ``_accessor_suffix`` (see the SKILL.md accessor table).
|
|
1354
|
+
|
|
1355
|
+
A bare reference to e.g. a Decision routing on a dcPicklist otherwise
|
|
1356
|
+
deploys with: "In a Decision Outcome, a condition doesn't support "X" Equals."
|
|
1357
|
+
|
|
1358
|
+
Returns a list of warnings for references the transformer could not safely
|
|
1359
|
+
qualify — a field whose accessor is ambiguous (dcName) or whose type could
|
|
1360
|
+
not be converted — so a silent bare reference is surfaced pre-deploy rather
|
|
1361
|
+
than discovered at dry-run.
|
|
1362
|
+
"""
|
|
1363
|
+
# Map each screen field name -> its DC accessor suffix (None = can't resolve).
|
|
1364
|
+
screen_field_suffix: Dict[str, Optional[str]] = {}
|
|
1365
|
+
for screen in findall(root, 'screens'):
|
|
1366
|
+
for field in findall(screen, 'fields'):
|
|
1367
|
+
fname = findtext(field, 'name')
|
|
1368
|
+
if fname:
|
|
1369
|
+
screen_field_suffix[fname] = _accessor_suffix(field)
|
|
1370
|
+
|
|
1371
|
+
if not screen_field_suffix:
|
|
1372
|
+
return []
|
|
1373
|
+
|
|
1374
|
+
warnings: List[str] = []
|
|
1375
|
+
warned: Set[str] = set()
|
|
1376
|
+
|
|
1377
|
+
def qualify(er_el: ET.Element):
|
|
1378
|
+
"""Qualify a bare screen-field reference in-place; warn if unresolvable."""
|
|
1379
|
+
name = er_el.text
|
|
1380
|
+
if not name or name not in screen_field_suffix:
|
|
1381
|
+
return # not a screen field, or already qualified — leave alone
|
|
1382
|
+
suffix = screen_field_suffix[name]
|
|
1383
|
+
if suffix is None:
|
|
1384
|
+
if name not in warned:
|
|
1385
|
+
warned.add(name)
|
|
1386
|
+
warnings.append(
|
|
1387
|
+
f"Screen field '{name}' is referenced downstream but its DC "
|
|
1388
|
+
f"accessor can't be auto-resolved (dcName is .firstName/"
|
|
1389
|
+
f".lastName; unconverted types have none). Qualify it "
|
|
1390
|
+
f"manually before deploy or the reference stays bare.")
|
|
1391
|
+
return
|
|
1392
|
+
er_el.text = f'{name}.{suffix}'
|
|
1393
|
+
|
|
1394
|
+
for ntype in ('recordCreates', 'recordUpdates', 'recordDeletes',
|
|
1395
|
+
'decisions', 'assignments', 'loops', 'subflows'):
|
|
1396
|
+
for node in findall(root, ntype):
|
|
1397
|
+
for er in node.iter(tag('elementReference')):
|
|
1398
|
+
qualify(er)
|
|
1399
|
+
for er in node.iter('elementReference'):
|
|
1400
|
+
qualify(er)
|
|
1401
|
+
|
|
1402
|
+
# Also fix decision leftValueReference elements. (rightValue is a container
|
|
1403
|
+
# of stringValue/elementReference children, not bare text — a screen-field
|
|
1404
|
+
# reference there arrives as a nested <elementReference>, already handled by
|
|
1405
|
+
# the 'decisions' branch above — so only leftValueReference is load-bearing.)
|
|
1406
|
+
for dec in findall(root, 'decisions'):
|
|
1407
|
+
for rule in findall(dec, 'rules'):
|
|
1408
|
+
for cond in findall(rule, 'conditions'):
|
|
1409
|
+
for lv in findall(cond, 'leftValueReference'):
|
|
1410
|
+
qualify(lv)
|
|
1411
|
+
|
|
1412
|
+
# Qualify {!merge} references to screen fields inside formula expressions and
|
|
1413
|
+
# DisplayText fieldText. A bare {!ClosingReading} (or a bare ClosingReading
|
|
1414
|
+
# inside a formula body like IF(ClosingReading > 100, ...)) leaves the flow
|
|
1415
|
+
# InvalidDraft in the org — and, critically, `sf deploy --dry-run` does NOT
|
|
1416
|
+
# catch it (reference validity is a separate Flow layer the dry-run skips), so
|
|
1417
|
+
# this must be fixed at transform time, not relied on at the validation gate.
|
|
1418
|
+
resolvable = {n: s for n, s in screen_field_suffix.items() if s is not None}
|
|
1419
|
+
|
|
1420
|
+
def qualify_merge_text(text: Optional[str]) -> Optional[str]:
|
|
1421
|
+
"""Return text with bare screen-field refs qualified by their accessor.
|
|
1422
|
+
Only rewrites a name when it is NOT already followed by '.' (so an
|
|
1423
|
+
already-qualified `.value` is left alone). Leaves definition elements
|
|
1424
|
+
untouched — this only runs on formula expressions / DisplayText."""
|
|
1425
|
+
if not text:
|
|
1426
|
+
return text
|
|
1427
|
+
for name, suffix in resolvable.items():
|
|
1428
|
+
esc = re.escape(name)
|
|
1429
|
+
# {!Field} -> {!Field.suffix} (merge-field form)
|
|
1430
|
+
text = re.sub(r'(\{!\s*)' + esc + r'(?!\s*\.)',
|
|
1431
|
+
lambda m: m.group(1) + name + '.' + suffix, text)
|
|
1432
|
+
# bare Field inside a formula body: IF(Field > ...) — not preceded by
|
|
1433
|
+
# '.' '!' or a word char, not followed by '.' or a word char.
|
|
1434
|
+
text = re.sub(r'(?<![.\w!])' + esc + r'(?![.\w])',
|
|
1435
|
+
name + '.' + suffix, text)
|
|
1436
|
+
return text
|
|
1437
|
+
|
|
1438
|
+
for fm in findall(root, 'formulas'):
|
|
1439
|
+
for expr in findall(fm, 'expression'):
|
|
1440
|
+
expr.text = qualify_merge_text(expr.text)
|
|
1441
|
+
|
|
1442
|
+
for screen in findall(root, 'screens'):
|
|
1443
|
+
for field in findall(screen, 'fields'):
|
|
1444
|
+
if findtext(field, 'fieldType') == 'DisplayText':
|
|
1445
|
+
for ft_el in findall(field, 'fieldText'):
|
|
1446
|
+
ft_el.text = qualify_merge_text(ft_el.text)
|
|
1447
|
+
|
|
1448
|
+
return warnings
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def remove_empty_connectors(root: ET.Element):
|
|
1452
|
+
"""Remove connector elements with empty or missing targetReference."""
|
|
1453
|
+
for ntype in ALL_TYPES:
|
|
1454
|
+
for node in findall(root, ntype):
|
|
1455
|
+
for conn_type in ('connector', 'defaultConnector',
|
|
1456
|
+
'nextValueConnector', 'noMoreValuesConnector'):
|
|
1457
|
+
for conn in findall(node, conn_type):
|
|
1458
|
+
tr = findtext(conn, 'targetReference')
|
|
1459
|
+
if not tr or not tr.strip():
|
|
1460
|
+
node.remove(conn)
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
def remove_platform_event_nodes(root: ET.Element) -> List[Dict]:
|
|
1464
|
+
"""
|
|
1465
|
+
Remove recordCreate nodes that target platform events (not UI-visible entities).
|
|
1466
|
+
DataCaptureFlow can only use objects that appear in the UI. Delegates the
|
|
1467
|
+
excise+placeholder+rewire+flag behavior to excise_and_placeholder so this
|
|
1468
|
+
site behaves identically to the CUD-excision and action-call-removal sites.
|
|
1469
|
+
"""
|
|
1470
|
+
def is_platform_event(obj_name: str) -> bool:
|
|
1471
|
+
# Platform events always end with __e suffix
|
|
1472
|
+
return (obj_name or '').endswith('__e')
|
|
1473
|
+
|
|
1474
|
+
flags: List[Dict] = []
|
|
1475
|
+
for rc in list(findall(root, 'recordCreates')):
|
|
1476
|
+
obj = findtext(rc, 'object') or ''
|
|
1477
|
+
rc_name = findtext(rc, 'name') or ''
|
|
1478
|
+
if not is_platform_event(obj):
|
|
1479
|
+
continue
|
|
1480
|
+
flags.extend(excise_and_placeholder(
|
|
1481
|
+
root, [rc_name], 'PLATFORM_EVENT_EXCISED',
|
|
1482
|
+
f'"{rc_name}" creates platform event "{obj}", which is not a '
|
|
1483
|
+
f'UI-visible entity and cannot be created from a DataCaptureFlow.'))
|
|
1484
|
+
return flags
|
|
1485
|
+
|
|
1486
|
+
|
|
1487
|
+
def remove_action_calls(root: ET.Element) -> List[Dict]:
|
|
1488
|
+
"""
|
|
1489
|
+
Remove <actionCalls> nodes — DataCaptureFlow can't include Action elements
|
|
1490
|
+
(deploy error: 'Flows of type "Data Capture Flow" can't include Action elements').
|
|
1491
|
+
Delegates to excise_and_placeholder for excise+placeholder+rewire+flag behavior,
|
|
1492
|
+
so a dropped side-effecting call (e.g. PostToChatter) is visible inline via the
|
|
1493
|
+
placeholder Screen, not just in the summary.
|
|
1494
|
+
"""
|
|
1495
|
+
flags: List[Dict] = []
|
|
1496
|
+
for ac in list(findall(root, 'actionCalls')):
|
|
1497
|
+
ac_name = findtext(ac, 'name') or ''
|
|
1498
|
+
ac_type = findtext(ac, 'actionType') or findtext(ac, 'actionName') or 'unknown'
|
|
1499
|
+
flags.extend(excise_and_placeholder(
|
|
1500
|
+
root, [ac_name], 'ACTION_CALL_EXCISED',
|
|
1501
|
+
f'Action elements (actionType "{ac_type}") are not allowed in '
|
|
1502
|
+
f'DataCaptureFlow; its side effect no longer fires. Re-express '
|
|
1503
|
+
f'user-facing notifications as a DisplayText field, or move the '
|
|
1504
|
+
f'action to a separate AutoLaunched/record-triggered flow.'))
|
|
1505
|
+
return flags
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def fix_subflow_references(root: ET.Element, dc_suffix: str = '_DC'):
|
|
1509
|
+
"""
|
|
1510
|
+
Update subflow flowName references to target the migrated DC versions.
|
|
1511
|
+
Delegates naming to subflow_names.migrated_dc_flow_name so discovery and
|
|
1512
|
+
transform stay in lockstep.
|
|
1513
|
+
"""
|
|
1514
|
+
for subflow in findall(root, 'subflows'):
|
|
1515
|
+
for fn in findall(subflow, 'flowName'):
|
|
1516
|
+
if not fn.text:
|
|
1517
|
+
continue
|
|
1518
|
+
fn.text = migrated_dc_flow_name(fn.text, dc_suffix)
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
# ──────────────────────────────────────────────
|
|
1522
|
+
# Main transform entry point
|
|
1523
|
+
# ──────────────────────────────────────────────
|
|
1524
|
+
|
|
1525
|
+
def transform(input_path: str, output_path: str, dc_suffix: str = '_DC', is_subflow: bool = False) -> dict:
|
|
1526
|
+
tree = ET.parse(input_path)
|
|
1527
|
+
root = tree.getroot()
|
|
1528
|
+
|
|
1529
|
+
label = findtext(root, 'label') or input_path
|
|
1530
|
+
process_type = findtext(root, 'processType') or ''
|
|
1531
|
+
|
|
1532
|
+
summary = {
|
|
1533
|
+
'label': label,
|
|
1534
|
+
'skipped': False,
|
|
1535
|
+
'skip_reason': '',
|
|
1536
|
+
'fields_converted': 0,
|
|
1537
|
+
'output_vars_mapped': 0,
|
|
1538
|
+
'assignment_nodes_injected': 0,
|
|
1539
|
+
'cud_fixes': 0,
|
|
1540
|
+
'cud_skipped': 0,
|
|
1541
|
+
'warnings': [],
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
# ── Skip pure automation flows (no screens) ──
|
|
1545
|
+
screens = findall(root, 'screens')
|
|
1546
|
+
if not screens:
|
|
1547
|
+
summary['skipped'] = True
|
|
1548
|
+
summary['skip_reason'] = (
|
|
1549
|
+
'Flow has no screen elements — it is a pure automation flow and '
|
|
1550
|
+
'cannot be a DataCaptureFlow, which requires at least one screen for user input. '
|
|
1551
|
+
'Keep this flow as FieldServiceMobile or convert to AutoLaunchedFlow.'
|
|
1552
|
+
)
|
|
1553
|
+
return summary
|
|
1554
|
+
|
|
1555
|
+
# Phase 1: metadata
|
|
1556
|
+
apply_dc_metadata(root)
|
|
1557
|
+
|
|
1558
|
+
# Phase 1b: remove actionCalls (not allowed in DC) before graph analysis, so
|
|
1559
|
+
# CUD-at-end enforcement sees the post-removal execution graph.
|
|
1560
|
+
action_call_flags = remove_action_calls(root)
|
|
1561
|
+
|
|
1562
|
+
# Phase 2: screen field transformation
|
|
1563
|
+
fields_converted, output_var_map = transform_screen_fields(root)
|
|
1564
|
+
summary['fields_converted'] = fields_converted
|
|
1565
|
+
summary['output_vars_mapped'] = len(output_var_map)
|
|
1566
|
+
|
|
1567
|
+
# Phase 3: inject assignment nodes to preserve outputAssignment variable bindings
|
|
1568
|
+
injected = inject_output_assignments(root, output_var_map)
|
|
1569
|
+
summary['assignment_nodes_injected'] = injected
|
|
1570
|
+
|
|
1571
|
+
# Phase 4: enforce CUD at end
|
|
1572
|
+
cud_fixed, cud_skipped, cud_warnings, cud_excision_flags = enforce_cud_at_end(root, is_subflow=is_subflow)
|
|
1573
|
+
summary['cud_fixes'] = cud_fixed
|
|
1574
|
+
summary['cud_skipped'] = cud_skipped
|
|
1575
|
+
summary['warnings'].extend(cud_warnings)
|
|
1576
|
+
|
|
1577
|
+
# Phase 5: structural DC requirements
|
|
1578
|
+
ensure_screen_attrs(root)
|
|
1579
|
+
ensure_node_locations(root)
|
|
1580
|
+
ensure_dc_variables(root)
|
|
1581
|
+
fix_subflow_references(root, dc_suffix)
|
|
1582
|
+
|
|
1583
|
+
# Phase 6: fix screen field references in downstream automation
|
|
1584
|
+
summary['warnings'].extend(fix_screen_field_references(root))
|
|
1585
|
+
|
|
1586
|
+
# Phase 7: remove dangling empty connectors (left by restructurer)
|
|
1587
|
+
remove_empty_connectors(root)
|
|
1588
|
+
|
|
1589
|
+
# Phase 8: remove platform event creates (not allowed in DC)
|
|
1590
|
+
platform_event_flags = remove_platform_event_nodes(root)
|
|
1591
|
+
|
|
1592
|
+
# Phase 9: reorder field children (inputParameters must be contiguous)
|
|
1593
|
+
reorder_field_children(root)
|
|
1594
|
+
|
|
1595
|
+
# Phase 10: detect manual-review flags
|
|
1596
|
+
flags = detect_manual_review_flags(root)
|
|
1597
|
+
summary['flags'] = cud_excision_flags + action_call_flags + platform_event_flags + flags
|
|
1598
|
+
|
|
1599
|
+
# Phase 11: regroup root children (excise_and_placeholder appends new
|
|
1600
|
+
# <screens> at the end, which can land after subflows/variables and
|
|
1601
|
+
# break the "same tag type must be contiguous" DC deploy rule)
|
|
1602
|
+
regroup_root_children(root)
|
|
1603
|
+
|
|
1604
|
+
# Write output
|
|
1605
|
+
ET.indent(tree, space=' ')
|
|
1606
|
+
tree.write(output_path, encoding='unicode', xml_declaration=True)
|
|
1607
|
+
with open(output_path) as f:
|
|
1608
|
+
content = f.read()
|
|
1609
|
+
if not content.startswith('<?xml'):
|
|
1610
|
+
content = '<?xml version="1.0" encoding="UTF-8"?>\n' + content
|
|
1611
|
+
with open(output_path, 'w') as f:
|
|
1612
|
+
f.write(content)
|
|
1613
|
+
|
|
1614
|
+
return summary
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def main():
|
|
1618
|
+
args = sys.argv[1:]
|
|
1619
|
+
dc_suffix = '_DC'
|
|
1620
|
+
if '--dc-suffix' in args:
|
|
1621
|
+
idx = args.index('--dc-suffix')
|
|
1622
|
+
dc_suffix = args[idx + 1]
|
|
1623
|
+
args = args[:idx] + args[idx + 2:]
|
|
1624
|
+
|
|
1625
|
+
is_subflow = '--is-subflow' in args
|
|
1626
|
+
if is_subflow:
|
|
1627
|
+
args = [a for a in args if a != '--is-subflow']
|
|
1628
|
+
|
|
1629
|
+
if len(args) != 2:
|
|
1630
|
+
print(f'Usage: {sys.argv[0]} <input.flow-meta.xml> <output.flow-meta.xml> [--dc-suffix SUFFIX] [--is-subflow]')
|
|
1631
|
+
sys.exit(1)
|
|
1632
|
+
|
|
1633
|
+
summary = transform(args[0], args[1], dc_suffix, is_subflow=is_subflow)
|
|
1634
|
+
|
|
1635
|
+
print(f"\n{'='*60}")
|
|
1636
|
+
if summary['skipped']:
|
|
1637
|
+
print(f"SKIPPED: {summary['label']}")
|
|
1638
|
+
print(f"Reason: {summary['skip_reason']}")
|
|
1639
|
+
else:
|
|
1640
|
+
print(f"Transformed: {summary['label']}")
|
|
1641
|
+
print(f"{'='*60}")
|
|
1642
|
+
print(f"Fields converted: {summary['fields_converted']}")
|
|
1643
|
+
print(f"Output vars mapped: {summary['output_vars_mapped']}")
|
|
1644
|
+
print(f"Assignment nodes injected: {summary['assignment_nodes_injected']}")
|
|
1645
|
+
print(f"CUD violations fixed: {summary['cud_fixes']}")
|
|
1646
|
+
print(f"CUD violations manual: {summary['cud_skipped']}")
|
|
1647
|
+
if summary['warnings']:
|
|
1648
|
+
print(f"\nWarnings ({len(summary['warnings'])}):")
|
|
1649
|
+
for w in summary['warnings']:
|
|
1650
|
+
print(f" - {w}")
|
|
1651
|
+
if summary.get('flags'):
|
|
1652
|
+
print(f"\n[WARN] Requires Manual Review ({len(summary['flags'])} pattern(s)):")
|
|
1653
|
+
for flag in summary['flags']:
|
|
1654
|
+
print(f" [{flag['code']}] {flag['what']}")
|
|
1655
|
+
print(f" Why: {flag['why']}")
|
|
1656
|
+
print(f" Fix: {flag['recommendation']}")
|
|
1657
|
+
print()
|
|
1658
|
+
|
|
1659
|
+
|
|
1660
|
+
if __name__ == '__main__':
|
|
1661
|
+
main()
|