@cohesivesystems/presentation-core 0.1.0-alpha.1
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/LICENSE +55 -0
- package/dist/api-scope-request.d.ts +43 -0
- package/dist/api-scope-request.d.ts.map +1 -0
- package/dist/api-scope-request.js +96 -0
- package/dist/api-scope-request.js.map +1 -0
- package/dist/collection-chrome-icons.d.ts +18 -0
- package/dist/collection-chrome-icons.d.ts.map +1 -0
- package/dist/collection-chrome-icons.js +57 -0
- package/dist/collection-chrome-icons.js.map +1 -0
- package/dist/collection-chrome-runtime.d.ts +25 -0
- package/dist/collection-chrome-runtime.d.ts.map +1 -0
- package/dist/collection-chrome-runtime.js +72 -0
- package/dist/collection-chrome-runtime.js.map +1 -0
- package/dist/collection-chrome-slot-renderer-registry.d.ts +110 -0
- package/dist/collection-chrome-slot-renderer-registry.d.ts.map +1 -0
- package/dist/collection-chrome-slot-renderer-registry.js +136 -0
- package/dist/collection-chrome-slot-renderer-registry.js.map +1 -0
- package/dist/collection-selection-state.d.ts +18 -0
- package/dist/collection-selection-state.d.ts.map +1 -0
- package/dist/collection-selection-state.js +2 -0
- package/dist/collection-selection-state.js.map +1 -0
- package/dist/data-source-projection.d.ts +475 -0
- package/dist/data-source-projection.d.ts.map +1 -0
- package/dist/data-source-projection.js +880 -0
- package/dist/data-source-projection.js.map +1 -0
- package/dist/data-source-query-lowering.d.ts +35 -0
- package/dist/data-source-query-lowering.d.ts.map +1 -0
- package/dist/data-source-query-lowering.js +104 -0
- package/dist/data-source-query-lowering.js.map +1 -0
- package/dist/date-time-filter-model.d.ts +20 -0
- package/dist/date-time-filter-model.d.ts.map +1 -0
- package/dist/date-time-filter-model.js +115 -0
- package/dist/date-time-filter-model.js.map +1 -0
- package/dist/document-badge-icons.d.ts +4 -0
- package/dist/document-badge-icons.d.ts.map +1 -0
- package/dist/document-badge-icons.js +4 -0
- package/dist/document-badge-icons.js.map +1 -0
- package/dist/document-module.d.ts +268 -0
- package/dist/document-module.d.ts.map +1 -0
- package/dist/document-module.js +347 -0
- package/dist/document-module.js.map +1 -0
- package/dist/document-summary-model.d.ts +2 -0
- package/dist/document-summary-model.d.ts.map +1 -0
- package/dist/document-summary-model.js +2 -0
- package/dist/document-summary-model.js.map +1 -0
- package/dist/document-workspace-chrome-projection.d.ts +18 -0
- package/dist/document-workspace-chrome-projection.d.ts.map +1 -0
- package/dist/document-workspace-chrome-projection.js +35 -0
- package/dist/document-workspace-chrome-projection.js.map +1 -0
- package/dist/document-workspace-surface-slots.d.ts +75 -0
- package/dist/document-workspace-surface-slots.d.ts.map +1 -0
- package/dist/document-workspace-surface-slots.js +268 -0
- package/dist/document-workspace-surface-slots.js.map +1 -0
- package/dist/endpoint-executor.d.ts +62 -0
- package/dist/endpoint-executor.d.ts.map +1 -0
- package/dist/endpoint-executor.js +35 -0
- package/dist/endpoint-executor.js.map +1 -0
- package/dist/field-display-resolution.d.ts +5 -0
- package/dist/field-display-resolution.d.ts.map +1 -0
- package/dist/field-display-resolution.js +50 -0
- package/dist/field-display-resolution.js.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +48 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +108 -0
- package/dist/module.js.map +1 -0
- package/dist/navigation-shell-frame-renderer-registry.d.ts +12 -0
- package/dist/navigation-shell-frame-renderer-registry.d.ts.map +1 -0
- package/dist/navigation-shell-frame-renderer-registry.js +29 -0
- package/dist/navigation-shell-frame-renderer-registry.js.map +1 -0
- package/dist/navigation-shell-layout-runtime.d.ts +29 -0
- package/dist/navigation-shell-layout-runtime.d.ts.map +1 -0
- package/dist/navigation-shell-layout-runtime.js +50 -0
- package/dist/navigation-shell-layout-runtime.js.map +1 -0
- package/dist/navigation-shell-region-component-registry.d.ts +96 -0
- package/dist/navigation-shell-region-component-registry.d.ts.map +1 -0
- package/dist/navigation-shell-region-component-registry.js +108 -0
- package/dist/navigation-shell-region-component-registry.js.map +1 -0
- package/dist/navigation-shell-runtime.d.ts +48 -0
- package/dist/navigation-shell-runtime.d.ts.map +1 -0
- package/dist/navigation-shell-runtime.js +457 -0
- package/dist/navigation-shell-runtime.js.map +1 -0
- package/dist/navigation-shell-slot-renderer-registry.d.ts +15 -0
- package/dist/navigation-shell-slot-renderer-registry.d.ts.map +1 -0
- package/dist/navigation-shell-slot-renderer-registry.js +48 -0
- package/dist/navigation-shell-slot-renderer-registry.js.map +1 -0
- package/dist/navigation.d.ts +84 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +167 -0
- package/dist/navigation.js.map +1 -0
- package/dist/object-path.d.ts +23 -0
- package/dist/object-path.d.ts.map +1 -0
- package/dist/object-path.js +63 -0
- package/dist/object-path.js.map +1 -0
- package/dist/page-host-renderer-registry.d.ts +119 -0
- package/dist/page-host-renderer-registry.d.ts.map +1 -0
- package/dist/page-host-renderer-registry.js +305 -0
- package/dist/page-host-renderer-registry.js.map +1 -0
- package/dist/presentation-action-endpoint-request-projection.d.ts +23 -0
- package/dist/presentation-action-endpoint-request-projection.d.ts.map +1 -0
- package/dist/presentation-action-endpoint-request-projection.js +117 -0
- package/dist/presentation-action-endpoint-request-projection.js.map +1 -0
- package/dist/presentation-action-endpoint.d.ts +24 -0
- package/dist/presentation-action-endpoint.d.ts.map +1 -0
- package/dist/presentation-action-endpoint.js +56 -0
- package/dist/presentation-action-endpoint.js.map +1 -0
- package/dist/presentation-action-icons.d.ts +4 -0
- package/dist/presentation-action-icons.d.ts.map +1 -0
- package/dist/presentation-action-icons.js +4 -0
- package/dist/presentation-action-icons.js.map +1 -0
- package/dist/presentation-action-result-state.d.ts +48 -0
- package/dist/presentation-action-result-state.d.ts.map +1 -0
- package/dist/presentation-action-result-state.js +109 -0
- package/dist/presentation-action-result-state.js.map +1 -0
- package/dist/presentation-action-runtime-binding.d.ts +48 -0
- package/dist/presentation-action-runtime-binding.d.ts.map +1 -0
- package/dist/presentation-action-runtime-binding.js +52 -0
- package/dist/presentation-action-runtime-binding.js.map +1 -0
- package/dist/presentation-action-runtime-diagnostics.d.ts +18 -0
- package/dist/presentation-action-runtime-diagnostics.d.ts.map +1 -0
- package/dist/presentation-action-runtime-diagnostics.js +69 -0
- package/dist/presentation-action-runtime-diagnostics.js.map +1 -0
- package/dist/presentation-action-runtime-labels.d.ts +15 -0
- package/dist/presentation-action-runtime-labels.d.ts.map +1 -0
- package/dist/presentation-action-runtime-labels.js +28 -0
- package/dist/presentation-action-runtime-labels.js.map +1 -0
- package/dist/presentation-action-runtime-projection.d.ts +17 -0
- package/dist/presentation-action-runtime-projection.d.ts.map +1 -0
- package/dist/presentation-action-runtime-projection.js +31 -0
- package/dist/presentation-action-runtime-projection.js.map +1 -0
- package/dist/presentation-badge-resolution.d.ts +8 -0
- package/dist/presentation-badge-resolution.d.ts.map +1 -0
- package/dist/presentation-badge-resolution.js +73 -0
- package/dist/presentation-badge-resolution.js.map +1 -0
- package/dist/presentation-collection-pagination.d.ts +32 -0
- package/dist/presentation-collection-pagination.d.ts.map +1 -0
- package/dist/presentation-collection-pagination.js +275 -0
- package/dist/presentation-collection-pagination.js.map +1 -0
- package/dist/presentation-content-diagnostics.d.ts +17 -0
- package/dist/presentation-content-diagnostics.d.ts.map +1 -0
- package/dist/presentation-content-diagnostics.js +61 -0
- package/dist/presentation-content-diagnostics.js.map +1 -0
- package/dist/presentation-content-resolution.d.ts +9 -0
- package/dist/presentation-content-resolution.d.ts.map +1 -0
- package/dist/presentation-content-resolution.js +19 -0
- package/dist/presentation-content-resolution.js.map +1 -0
- package/dist/presentation-data-source-binding-model.d.ts +81 -0
- package/dist/presentation-data-source-binding-model.d.ts.map +1 -0
- package/dist/presentation-data-source-binding-model.js +56 -0
- package/dist/presentation-data-source-binding-model.js.map +1 -0
- package/dist/presentation-data-source-runtime.d.ts +62 -0
- package/dist/presentation-data-source-runtime.d.ts.map +1 -0
- package/dist/presentation-data-source-runtime.js +80 -0
- package/dist/presentation-data-source-runtime.js.map +1 -0
- package/dist/presentation-design-intent-diagnostics.d.ts +25 -0
- package/dist/presentation-design-intent-diagnostics.d.ts.map +1 -0
- package/dist/presentation-design-intent-diagnostics.js +55 -0
- package/dist/presentation-design-intent-diagnostics.js.map +1 -0
- package/dist/presentation-icon-diagnostics.d.ts +45 -0
- package/dist/presentation-icon-diagnostics.d.ts.map +1 -0
- package/dist/presentation-icon-diagnostics.js +235 -0
- package/dist/presentation-icon-diagnostics.js.map +1 -0
- package/dist/presentation-icon-registry.d.ts +36 -0
- package/dist/presentation-icon-registry.d.ts.map +1 -0
- package/dist/presentation-icon-registry.js +81 -0
- package/dist/presentation-icon-registry.js.map +1 -0
- package/dist/presentation-navigation-action-runtime-bindings.d.ts +13 -0
- package/dist/presentation-navigation-action-runtime-bindings.d.ts.map +1 -0
- package/dist/presentation-navigation-action-runtime-bindings.js +29 -0
- package/dist/presentation-navigation-action-runtime-bindings.js.map +1 -0
- package/dist/presentation-pagination.d.ts +98 -0
- package/dist/presentation-pagination.d.ts.map +1 -0
- package/dist/presentation-pagination.js +427 -0
- package/dist/presentation-pagination.js.map +1 -0
- package/dist/presentation-projection-coverage.d.ts +38 -0
- package/dist/presentation-projection-coverage.d.ts.map +1 -0
- package/dist/presentation-projection-coverage.js +2429 -0
- package/dist/presentation-projection-coverage.js.map +1 -0
- package/dist/presentation-projection-diagnostics.d.ts +33 -0
- package/dist/presentation-projection-diagnostics.d.ts.map +1 -0
- package/dist/presentation-projection-diagnostics.js +17 -0
- package/dist/presentation-projection-diagnostics.js.map +1 -0
- package/dist/presentation-projection-trace.d.ts +97 -0
- package/dist/presentation-projection-trace.d.ts.map +1 -0
- package/dist/presentation-projection-trace.js +110 -0
- package/dist/presentation-projection-trace.js.map +1 -0
- package/dist/presentation-query-form-state.d.ts +15 -0
- package/dist/presentation-query-form-state.d.ts.map +1 -0
- package/dist/presentation-query-form-state.js +20 -0
- package/dist/presentation-query-form-state.js.map +1 -0
- package/dist/presentation-renderer-registry.d.ts +86 -0
- package/dist/presentation-renderer-registry.d.ts.map +1 -0
- package/dist/presentation-renderer-registry.js +108 -0
- package/dist/presentation-renderer-registry.js.map +1 -0
- package/dist/presentation-semantics.d.ts +288 -0
- package/dist/presentation-semantics.d.ts.map +1 -0
- package/dist/presentation-semantics.js +560 -0
- package/dist/presentation-semantics.js.map +1 -0
- package/dist/presentation-standards.d.ts +8 -0
- package/dist/presentation-standards.d.ts.map +1 -0
- package/dist/presentation-standards.js +9 -0
- package/dist/presentation-standards.js.map +1 -0
- package/dist/presentation-test-selectors.d.ts +146 -0
- package/dist/presentation-test-selectors.d.ts.map +1 -0
- package/dist/presentation-test-selectors.js +113 -0
- package/dist/presentation-test-selectors.js.map +1 -0
- package/dist/presentation-value-formatting.d.ts +12 -0
- package/dist/presentation-value-formatting.d.ts.map +1 -0
- package/dist/presentation-value-formatting.js +33 -0
- package/dist/presentation-value-formatting.js.map +1 -0
- package/dist/presentation-value-resolution.d.ts +7 -0
- package/dist/presentation-value-resolution.d.ts.map +1 -0
- package/dist/presentation-value-resolution.js +69 -0
- package/dist/presentation-value-resolution.js.map +1 -0
- package/dist/presentation-view-tree.d.ts +6 -0
- package/dist/presentation-view-tree.d.ts.map +1 -0
- package/dist/presentation-view-tree.js +15 -0
- package/dist/presentation-view-tree.js.map +1 -0
- package/dist/process-task-model.d.ts +80 -0
- package/dist/process-task-model.d.ts.map +1 -0
- package/dist/process-task-model.js +52 -0
- package/dist/process-task-model.js.map +1 -0
- package/dist/process-task-projection.d.ts +46 -0
- package/dist/process-task-projection.d.ts.map +1 -0
- package/dist/process-task-projection.js +233 -0
- package/dist/process-task-projection.js.map +1 -0
- package/dist/projected-action-status-projection.d.ts +18 -0
- package/dist/projected-action-status-projection.d.ts.map +1 -0
- package/dist/projected-action-status-projection.js +19 -0
- package/dist/projected-action-status-projection.js.map +1 -0
- package/dist/projected-activity-state-model.d.ts +22 -0
- package/dist/projected-activity-state-model.d.ts.map +1 -0
- package/dist/projected-activity-state-model.js +87 -0
- package/dist/projected-activity-state-model.js.map +1 -0
- package/dist/projected-collection-action-runtime-bindings.d.ts +12 -0
- package/dist/projected-collection-action-runtime-bindings.d.ts.map +1 -0
- package/dist/projected-collection-action-runtime-bindings.js +37 -0
- package/dist/projected-collection-action-runtime-bindings.js.map +1 -0
- package/dist/projected-collection-action-runtime-diagnostics.d.ts +23 -0
- package/dist/projected-collection-action-runtime-diagnostics.d.ts.map +1 -0
- package/dist/projected-collection-action-runtime-diagnostics.js +168 -0
- package/dist/projected-collection-action-runtime-diagnostics.js.map +1 -0
- package/dist/projected-collection-runtime.d.ts +475 -0
- package/dist/projected-collection-runtime.d.ts.map +1 -0
- package/dist/projected-collection-runtime.js +832 -0
- package/dist/projected-collection-runtime.js.map +1 -0
- package/dist/projected-document-action-diagnostics.d.ts +16 -0
- package/dist/projected-document-action-diagnostics.d.ts.map +1 -0
- package/dist/projected-document-action-diagnostics.js +113 -0
- package/dist/projected-document-action-diagnostics.js.map +1 -0
- package/dist/projected-document-action-enablements.d.ts +25 -0
- package/dist/projected-document-action-enablements.d.ts.map +1 -0
- package/dist/projected-document-action-enablements.js +38 -0
- package/dist/projected-document-action-enablements.js.map +1 -0
- package/dist/projected-document-action-projection.d.ts +45 -0
- package/dist/projected-document-action-projection.d.ts.map +1 -0
- package/dist/projected-document-action-projection.js +201 -0
- package/dist/projected-document-action-projection.js.map +1 -0
- package/dist/projected-document-action-runtime-profile-diagnostics.d.ts +16 -0
- package/dist/projected-document-action-runtime-profile-diagnostics.d.ts.map +1 -0
- package/dist/projected-document-action-runtime-profile-diagnostics.js +168 -0
- package/dist/projected-document-action-runtime-profile-diagnostics.js.map +1 -0
- package/dist/projected-document-action-runtime-profile.d.ts +64 -0
- package/dist/projected-document-action-runtime-profile.d.ts.map +1 -0
- package/dist/projected-document-action-runtime-profile.js +33 -0
- package/dist/projected-document-action-runtime-profile.js.map +1 -0
- package/dist/projected-document-action-status-notice-diagnostics.d.ts +10 -0
- package/dist/projected-document-action-status-notice-diagnostics.d.ts.map +1 -0
- package/dist/projected-document-action-status-notice-diagnostics.js +64 -0
- package/dist/projected-document-action-status-notice-diagnostics.js.map +1 -0
- package/dist/projected-document-badge-formatting.d.ts +2 -0
- package/dist/projected-document-badge-formatting.d.ts.map +1 -0
- package/dist/projected-document-badge-formatting.js +7 -0
- package/dist/projected-document-badge-formatting.js.map +1 -0
- package/dist/projected-document-editor-model.d.ts +4 -0
- package/dist/projected-document-editor-model.d.ts.map +1 -0
- package/dist/projected-document-editor-model.js +2 -0
- package/dist/projected-document-editor-model.js.map +1 -0
- package/dist/projected-document-field-navigation.d.ts +72 -0
- package/dist/projected-document-field-navigation.d.ts.map +1 -0
- package/dist/projected-document-field-navigation.js +143 -0
- package/dist/projected-document-field-navigation.js.map +1 -0
- package/dist/projected-document-field-semantics.d.ts +5 -0
- package/dist/projected-document-field-semantics.d.ts.map +1 -0
- package/dist/projected-document-field-semantics.js +16 -0
- package/dist/projected-document-field-semantics.js.map +1 -0
- package/dist/projected-document-workspace-data-source-bindings.d.ts +69 -0
- package/dist/projected-document-workspace-data-source-bindings.d.ts.map +1 -0
- package/dist/projected-document-workspace-data-source-bindings.js +81 -0
- package/dist/projected-document-workspace-data-source-bindings.js.map +1 -0
- package/dist/projected-document-workspace-notice-diagnostics.d.ts +12 -0
- package/dist/projected-document-workspace-notice-diagnostics.d.ts.map +1 -0
- package/dist/projected-document-workspace-notice-diagnostics.js +13 -0
- package/dist/projected-document-workspace-notice-diagnostics.js.map +1 -0
- package/dist/projected-input-form-runtime.d.ts +62 -0
- package/dist/projected-input-form-runtime.d.ts.map +1 -0
- package/dist/projected-input-form-runtime.js +2 -0
- package/dist/projected-input-form-runtime.js.map +1 -0
- package/dist/projected-process-task-notice-diagnostics.d.ts +13 -0
- package/dist/projected-process-task-notice-diagnostics.d.ts.map +1 -0
- package/dist/projected-process-task-notice-diagnostics.js +170 -0
- package/dist/projected-process-task-notice-diagnostics.js.map +1 -0
- package/dist/projected-query-form-runtime.d.ts +46 -0
- package/dist/projected-query-form-runtime.d.ts.map +1 -0
- package/dist/projected-query-form-runtime.js +2 -0
- package/dist/projected-query-form-runtime.js.map +1 -0
- package/dist/prompt-document-preview-projection.d.ts +46 -0
- package/dist/prompt-document-preview-projection.d.ts.map +1 -0
- package/dist/prompt-document-preview-projection.js +173 -0
- package/dist/prompt-document-preview-projection.js.map +1 -0
- package/dist/prompt-icons.d.ts +4 -0
- package/dist/prompt-icons.d.ts.map +1 -0
- package/dist/prompt-icons.js +4 -0
- package/dist/prompt-icons.js.map +1 -0
- package/dist/prompt-status-message-resolution.d.ts +18 -0
- package/dist/prompt-status-message-resolution.d.ts.map +1 -0
- package/dist/prompt-status-message-resolution.js +83 -0
- package/dist/prompt-status-message-resolution.js.map +1 -0
- package/dist/query-form-lowering.d.ts +16 -0
- package/dist/query-form-lowering.d.ts.map +1 -0
- package/dist/query-form-lowering.js +38 -0
- package/dist/query-form-lowering.js.map +1 -0
- package/dist/query-form-url-state.d.ts +44 -0
- package/dist/query-form-url-state.d.ts.map +1 -0
- package/dist/query-form-url-state.js +197 -0
- package/dist/query-form-url-state.js.map +1 -0
- package/dist/query-offset-page.d.ts +13 -0
- package/dist/query-offset-page.d.ts.map +1 -0
- package/dist/query-offset-page.js +24 -0
- package/dist/query-offset-page.js.map +1 -0
- package/dist/relation-query-input-form-target.d.ts +10 -0
- package/dist/relation-query-input-form-target.d.ts.map +1 -0
- package/dist/relation-query-input-form-target.js +30 -0
- package/dist/relation-query-input-form-target.js.map +1 -0
- package/dist/scope-selection.d.ts +94 -0
- package/dist/scope-selection.d.ts.map +1 -0
- package/dist/scope-selection.js +95 -0
- package/dist/scope-selection.js.map +1 -0
- package/dist/target-bindings.d.ts +39 -0
- package/dist/target-bindings.d.ts.map +1 -0
- package/dist/target-bindings.js +40 -0
- package/dist/target-bindings.js.map +1 -0
- package/dist/view-chrome-content-diagnostics.d.ts +8 -0
- package/dist/view-chrome-content-diagnostics.d.ts.map +1 -0
- package/dist/view-chrome-content-diagnostics.js +84 -0
- package/dist/view-chrome-content-diagnostics.js.map +1 -0
- package/dist/view-chrome-icons.d.ts +12 -0
- package/dist/view-chrome-icons.d.ts.map +1 -0
- package/dist/view-chrome-icons.js +50 -0
- package/dist/view-chrome-icons.js.map +1 -0
- package/dist/view-chrome-slot-renderer-registry.d.ts +19 -0
- package/dist/view-chrome-slot-renderer-registry.d.ts.map +1 -0
- package/dist/view-chrome-slot-renderer-registry.js +62 -0
- package/dist/view-chrome-slot-renderer-registry.js.map +1 -0
- package/package.json +53 -0
- package/src/api-scope-request.ts +186 -0
- package/src/collection-chrome-icons.ts +89 -0
- package/src/collection-chrome-runtime.ts +151 -0
- package/src/collection-chrome-slot-renderer-registry.ts +227 -0
- package/src/collection-selection-state.ts +32 -0
- package/src/data-source-projection.ts +2149 -0
- package/src/data-source-query-lowering.ts +217 -0
- package/src/date-time-filter-model.ts +156 -0
- package/src/document-badge-icons.ts +3 -0
- package/src/document-module.ts +741 -0
- package/src/document-summary-model.ts +1 -0
- package/src/document-workspace-chrome-projection.ts +62 -0
- package/src/document-workspace-surface-slots.ts +458 -0
- package/src/endpoint-executor.ts +122 -0
- package/src/field-display-resolution.ts +99 -0
- package/src/index.ts +104 -0
- package/src/module.ts +281 -0
- package/src/navigation-shell-frame-renderer-registry.ts +65 -0
- package/src/navigation-shell-layout-runtime.ts +124 -0
- package/src/navigation-shell-region-component-registry.ts +242 -0
- package/src/navigation-shell-runtime.ts +579 -0
- package/src/navigation-shell-slot-renderer-registry.ts +94 -0
- package/src/navigation.ts +364 -0
- package/src/object-path.ts +99 -0
- package/src/page-host-renderer-registry.ts +615 -0
- package/src/presentation-action-endpoint-request-projection.ts +223 -0
- package/src/presentation-action-endpoint.ts +130 -0
- package/src/presentation-action-icons.ts +3 -0
- package/src/presentation-action-result-state.ts +189 -0
- package/src/presentation-action-runtime-binding.ts +177 -0
- package/src/presentation-action-runtime-diagnostics.ts +120 -0
- package/src/presentation-action-runtime-labels.ts +57 -0
- package/src/presentation-action-runtime-projection.ts +67 -0
- package/src/presentation-badge-resolution.ts +138 -0
- package/src/presentation-collection-pagination.ts +436 -0
- package/src/presentation-content-diagnostics.ts +121 -0
- package/src/presentation-content-resolution.ts +46 -0
- package/src/presentation-data-source-binding-model.ts +140 -0
- package/src/presentation-data-source-runtime.ts +190 -0
- package/src/presentation-design-intent-diagnostics.ts +104 -0
- package/src/presentation-icon-diagnostics.ts +368 -0
- package/src/presentation-icon-registry.ts +181 -0
- package/src/presentation-navigation-action-runtime-bindings.ts +54 -0
- package/src/presentation-pagination.ts +694 -0
- package/src/presentation-projection-coverage.ts +3257 -0
- package/src/presentation-projection-diagnostics.ts +71 -0
- package/src/presentation-projection-trace.ts +293 -0
- package/src/presentation-query-form-state.ts +54 -0
- package/src/presentation-renderer-registry.ts +269 -0
- package/src/presentation-semantics.ts +1028 -0
- package/src/presentation-standards.ts +14 -0
- package/src/presentation-test-selectors.ts +312 -0
- package/src/presentation-value-formatting.ts +65 -0
- package/src/presentation-value-resolution.ts +100 -0
- package/src/presentation-view-tree.ts +33 -0
- package/src/process-task-model.ts +158 -0
- package/src/process-task-projection.ts +398 -0
- package/src/projected-action-status-projection.ts +43 -0
- package/src/projected-activity-state-model.ts +123 -0
- package/src/projected-collection-action-runtime-bindings.ts +76 -0
- package/src/projected-collection-action-runtime-diagnostics.ts +289 -0
- package/src/projected-collection-runtime.ts +1772 -0
- package/src/projected-document-action-diagnostics.ts +196 -0
- package/src/projected-document-action-enablements.ts +99 -0
- package/src/projected-document-action-projection.ts +464 -0
- package/src/projected-document-action-runtime-profile-diagnostics.ts +250 -0
- package/src/projected-document-action-runtime-profile.ts +133 -0
- package/src/projected-document-action-status-notice-diagnostics.ts +116 -0
- package/src/projected-document-badge-formatting.ts +7 -0
- package/src/projected-document-editor-model.ts +7 -0
- package/src/projected-document-field-navigation.ts +300 -0
- package/src/projected-document-field-semantics.ts +27 -0
- package/src/projected-document-workspace-data-source-bindings.ts +182 -0
- package/src/projected-document-workspace-notice-diagnostics.ts +41 -0
- package/src/projected-input-form-runtime.ts +86 -0
- package/src/projected-process-task-notice-diagnostics.ts +283 -0
- package/src/projected-query-form-runtime.ts +62 -0
- package/src/prompt-document-preview-projection.ts +345 -0
- package/src/prompt-icons.ts +3 -0
- package/src/prompt-status-message-resolution.ts +158 -0
- package/src/query-form-lowering.ts +77 -0
- package/src/query-form-url-state.ts +346 -0
- package/src/query-offset-page.ts +50 -0
- package/src/relation-query-input-form-target.ts +61 -0
- package/src/scope-selection.ts +201 -0
- package/src/target-bindings.ts +123 -0
- package/src/view-chrome-content-diagnostics.ts +139 -0
- package/src/view-chrome-icons.ts +81 -0
- package/src/view-chrome-slot-renderer-registry.ts +128 -0
|
@@ -0,0 +1,3257 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActionDefinition,
|
|
3
|
+
ActionPlacementDefinition,
|
|
4
|
+
CollectionChromeSlotDefinition,
|
|
5
|
+
CollectionRowActionDefinition,
|
|
6
|
+
CollectionSelectionActionDefinition,
|
|
7
|
+
InputFormDefinition,
|
|
8
|
+
PresentationModuleDefinition,
|
|
9
|
+
PresentationValueDefinition,
|
|
10
|
+
QueryFormDefinition,
|
|
11
|
+
ViewChromeSlotDefinition,
|
|
12
|
+
ViewDefinition,
|
|
13
|
+
} from './module'
|
|
14
|
+
import {
|
|
15
|
+
findPresentationAction,
|
|
16
|
+
findPresentationDataSource,
|
|
17
|
+
findPresentationField,
|
|
18
|
+
findPresentationFlow,
|
|
19
|
+
findPresentationInputForm,
|
|
20
|
+
findPresentationQueryForm,
|
|
21
|
+
findPresentationView,
|
|
22
|
+
} from './module'
|
|
23
|
+
import type {
|
|
24
|
+
PresentationDataSourceTargetInterpretation,
|
|
25
|
+
} from './data-source-projection'
|
|
26
|
+
import type {
|
|
27
|
+
PresentationDataSourceBinding,
|
|
28
|
+
} from './presentation-data-source-binding-model'
|
|
29
|
+
import {
|
|
30
|
+
presentationDataSourceBindingKinds,
|
|
31
|
+
} from './presentation-data-source-binding-model'
|
|
32
|
+
import {
|
|
33
|
+
getCollectionChromeSlotRendererCandidateKeys,
|
|
34
|
+
hasCollectionChromeSlotRendererBinding,
|
|
35
|
+
} from './collection-chrome-slot-renderer-registry'
|
|
36
|
+
import {
|
|
37
|
+
getViewChromeSlotRendererCandidateKeys,
|
|
38
|
+
hasViewChromeSlotRendererBinding,
|
|
39
|
+
isViewChromeSlotKind,
|
|
40
|
+
} from './view-chrome-slot-renderer-registry'
|
|
41
|
+
import {
|
|
42
|
+
createCollectionChromeRuntime,
|
|
43
|
+
isCollectionChromeSlotKind,
|
|
44
|
+
isCollectionChromeSlotPlacement,
|
|
45
|
+
} from './collection-chrome-runtime'
|
|
46
|
+
import {
|
|
47
|
+
getPresentationViewProjectedActions,
|
|
48
|
+
getPresentationViewProjectedFieldRefs,
|
|
49
|
+
type PresentationViewProjectedActionRef,
|
|
50
|
+
} from './presentation-semantics'
|
|
51
|
+
import type {
|
|
52
|
+
PresentationProjectionDiagnostic,
|
|
53
|
+
} from './presentation-projection-diagnostics'
|
|
54
|
+
import {
|
|
55
|
+
collectionChromeProjectionModes,
|
|
56
|
+
isCollectionChromeSlotProjectionMode,
|
|
57
|
+
readCollectionChromeProjectionMode,
|
|
58
|
+
resolveProjectedCollectionChromeSlots,
|
|
59
|
+
} from './projected-collection-runtime'
|
|
60
|
+
import type {
|
|
61
|
+
PresentationProjectionTrace,
|
|
62
|
+
PresentationProjectionTraceView,
|
|
63
|
+
} from './presentation-projection-trace'
|
|
64
|
+
import {
|
|
65
|
+
actionKinds,
|
|
66
|
+
collectionChromeSlotKinds,
|
|
67
|
+
collectionChromeSlotPlacements,
|
|
68
|
+
collectionDetailActivations,
|
|
69
|
+
collectionRowActionKinds,
|
|
70
|
+
collectionSelectionActionParameterSources,
|
|
71
|
+
collectionSelectionModes,
|
|
72
|
+
dataSourceKinds,
|
|
73
|
+
presentationValueKinds,
|
|
74
|
+
pageHostComponentRoles,
|
|
75
|
+
presentationBindingKinds,
|
|
76
|
+
viewKinds,
|
|
77
|
+
viewChromeSlotKinds,
|
|
78
|
+
} from '@cohesivesystems/presentation-contracts'
|
|
79
|
+
|
|
80
|
+
export interface ProjectPresentationTraceCoverageDiagnosticsOptions {
|
|
81
|
+
readonly collectionChromeSlotRendererKeys?: readonly string[]
|
|
82
|
+
readonly collectionSelectionStateRuntimeBound?: boolean
|
|
83
|
+
readonly module: PresentationProjectionCoverageModule | null
|
|
84
|
+
readonly queryFormStateAdapterIds?: readonly string[]
|
|
85
|
+
readonly trace: PresentationProjectionTrace
|
|
86
|
+
readonly viewChromeSlotRendererKeys?: readonly string[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ProjectPresentationDataSourceCoverageDiagnosticsOptions {
|
|
90
|
+
readonly bindings: readonly PresentationDataSourceBinding[]
|
|
91
|
+
readonly dataSourceIds: readonly string[]
|
|
92
|
+
readonly module: Pick<PresentationModuleDefinition, 'DataSources'> | null
|
|
93
|
+
readonly routeParameters?: Readonly<Record<string, string | undefined>>
|
|
94
|
+
readonly sourceId: string
|
|
95
|
+
readonly targetInterpretation?: Pick<PresentationDataSourceTargetInterpretation, 'queryLowering'>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type PresentationProjectionCoverageModule =
|
|
99
|
+
{
|
|
100
|
+
readonly Actions?: readonly PresentationModuleDefinition['Actions'][number][]
|
|
101
|
+
readonly DataSources?: readonly PresentationModuleDefinition['DataSources'][number][]
|
|
102
|
+
readonly Fields?: readonly PresentationModuleDefinition['Fields'][number][]
|
|
103
|
+
readonly Flows?: readonly PresentationModuleDefinition['Flows'][number][]
|
|
104
|
+
readonly InputForms?: readonly PresentationModuleDefinition['InputForms'][number][]
|
|
105
|
+
readonly QueryForms?: readonly PresentationModuleDefinition['QueryForms'][number][]
|
|
106
|
+
readonly Views: readonly PresentationModuleDefinition['Views'][number][]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Checks active route, page-host, view renderer, action, and input-form coverage
|
|
111
|
+
* using the same semantic trace that the developer toolbar displays.
|
|
112
|
+
*/
|
|
113
|
+
export function projectPresentationTraceCoverageDiagnostics({
|
|
114
|
+
collectionChromeSlotRendererKeys = [],
|
|
115
|
+
collectionSelectionStateRuntimeBound = false,
|
|
116
|
+
module,
|
|
117
|
+
queryFormStateAdapterIds = [],
|
|
118
|
+
trace,
|
|
119
|
+
viewChromeSlotRendererKeys = [],
|
|
120
|
+
}: ProjectPresentationTraceCoverageDiagnosticsOptions): readonly PresentationProjectionDiagnostic[] {
|
|
121
|
+
return [
|
|
122
|
+
...projectRouteCoverageDiagnostics(trace),
|
|
123
|
+
...projectViewRendererCoverageDiagnostics(trace),
|
|
124
|
+
...projectViewModelCoverageDiagnostics(
|
|
125
|
+
module,
|
|
126
|
+
trace,
|
|
127
|
+
queryFormStateAdapterIds,
|
|
128
|
+
collectionSelectionStateRuntimeBound,
|
|
129
|
+
collectionChromeSlotRendererKeys,
|
|
130
|
+
viewChromeSlotRendererKeys,
|
|
131
|
+
),
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Checks concrete data-source bindings projected for a routed surface. */
|
|
136
|
+
export function projectPresentationDataSourceCoverageDiagnostics({
|
|
137
|
+
bindings,
|
|
138
|
+
dataSourceIds,
|
|
139
|
+
module,
|
|
140
|
+
routeParameters = {},
|
|
141
|
+
sourceId,
|
|
142
|
+
targetInterpretation,
|
|
143
|
+
}: ProjectPresentationDataSourceCoverageDiagnosticsOptions): readonly PresentationProjectionDiagnostic[] {
|
|
144
|
+
const bindingByDataSourceId = new Map(bindings.map((binding) => [binding.dataSourceId, binding]))
|
|
145
|
+
|
|
146
|
+
return Array.from(new Set(dataSourceIds)).flatMap((dataSourceId) => {
|
|
147
|
+
const dataSource = findPresentationDataSource(module, dataSourceId)
|
|
148
|
+
if (!dataSource) {
|
|
149
|
+
return [
|
|
150
|
+
createDiagnostic({
|
|
151
|
+
id: `data-source.${dataSourceId}.missing-definition`,
|
|
152
|
+
message: `Data source '${dataSourceId}' is referenced by the active surface but is not present in the presentation module.`,
|
|
153
|
+
severity: 'error',
|
|
154
|
+
source: sourceId,
|
|
155
|
+
subject: {
|
|
156
|
+
id: dataSourceId,
|
|
157
|
+
kind: 'data-source',
|
|
158
|
+
},
|
|
159
|
+
}),
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const binding = bindingByDataSourceId.get(dataSourceId)
|
|
164
|
+
if (!binding) {
|
|
165
|
+
return [
|
|
166
|
+
createDiagnostic({
|
|
167
|
+
category: 'missing-binding',
|
|
168
|
+
id: `data-source.${dataSourceId}.missing-binding`,
|
|
169
|
+
interpretation: {
|
|
170
|
+
status: 'unbound',
|
|
171
|
+
target: 'data-source-binding',
|
|
172
|
+
},
|
|
173
|
+
message: `Data source '${dataSource.Name}' has no projected frontend binding.`,
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
source: sourceId,
|
|
176
|
+
subject: {
|
|
177
|
+
id: dataSource.Id,
|
|
178
|
+
kind: 'data-source',
|
|
179
|
+
name: dataSource.Name,
|
|
180
|
+
},
|
|
181
|
+
suggestedNextStep:
|
|
182
|
+
'Register a data-source binding factory or add a generic target interpreter for this data source kind.',
|
|
183
|
+
}),
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
188
|
+
diagnostics.push(
|
|
189
|
+
...projectDataSourceQueryLoweringTransformDiagnostics({
|
|
190
|
+
dataSource,
|
|
191
|
+
sourceId,
|
|
192
|
+
targetInterpretation,
|
|
193
|
+
}),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if (isUnboundDataSourceBinding(binding)) {
|
|
197
|
+
diagnostics.push(createDiagnostic({
|
|
198
|
+
category: 'missing-binding',
|
|
199
|
+
details: { bindingKind: binding.kind },
|
|
200
|
+
id: `data-source.${dataSource.Id}.unbound-frontend-binding`,
|
|
201
|
+
interpretation: {
|
|
202
|
+
status: 'unbound',
|
|
203
|
+
target: 'data-source-binding-factory',
|
|
204
|
+
},
|
|
205
|
+
message: `Data source '${dataSource.Name}' is declared by the IR but has no frontend binding factory.`,
|
|
206
|
+
severity: 'warning',
|
|
207
|
+
source: sourceId,
|
|
208
|
+
subject: {
|
|
209
|
+
id: dataSource.Id,
|
|
210
|
+
kind: 'data-source',
|
|
211
|
+
name: dataSource.Name,
|
|
212
|
+
},
|
|
213
|
+
suggestedNextStep:
|
|
214
|
+
'Bind this data source through backend-declared query lowering or an explicit frontend adapter.',
|
|
215
|
+
}))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const missingRequiredParameters = dataSource.Parameters.filter((parameter) =>
|
|
219
|
+
parameter.IsRequired &&
|
|
220
|
+
!parameter.DefaultValue &&
|
|
221
|
+
!routeParameters[parameter.Name])
|
|
222
|
+
if (
|
|
223
|
+
missingRequiredParameters.length > 0 &&
|
|
224
|
+
binding.kind === presentationDataSourceBindingKinds.tanstackQuery &&
|
|
225
|
+
binding.enabled === false
|
|
226
|
+
) {
|
|
227
|
+
diagnostics.push(createDiagnostic({
|
|
228
|
+
details: {
|
|
229
|
+
missingParameters: missingRequiredParameters.map((parameter) => parameter.Name),
|
|
230
|
+
},
|
|
231
|
+
id: `data-source.${dataSource.Id}.missing-route-parameters`,
|
|
232
|
+
message:
|
|
233
|
+
`Data source '${dataSource.Name}' is bound but disabled because required ` +
|
|
234
|
+
`route parameter(s) are missing: ${missingRequiredParameters
|
|
235
|
+
.map((parameter) => parameter.Name)
|
|
236
|
+
.join(', ')}.`,
|
|
237
|
+
severity: 'warning',
|
|
238
|
+
source: sourceId,
|
|
239
|
+
subject: {
|
|
240
|
+
id: dataSource.Id,
|
|
241
|
+
kind: 'data-source',
|
|
242
|
+
name: dataSource.Name,
|
|
243
|
+
},
|
|
244
|
+
}))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return diagnostics
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function projectRouteCoverageDiagnostics(
|
|
252
|
+
trace: PresentationProjectionTrace,
|
|
253
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
254
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
255
|
+
if (!trace.moduleAvailable) {
|
|
256
|
+
diagnostics.push(createDiagnostic({
|
|
257
|
+
id: 'route.missing-module',
|
|
258
|
+
message: 'Presentation module is not available for the active route.',
|
|
259
|
+
severity: 'error',
|
|
260
|
+
source: 'presentation-route-coverage',
|
|
261
|
+
subject: {
|
|
262
|
+
id: trace.pathname,
|
|
263
|
+
kind: 'route',
|
|
264
|
+
},
|
|
265
|
+
}))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!trace.route) {
|
|
269
|
+
diagnostics.push(createDiagnostic({
|
|
270
|
+
id: `route.${trace.pathname}.unmatched`,
|
|
271
|
+
message: `No presentation route matched '${trace.pathname}'.`,
|
|
272
|
+
severity: 'error',
|
|
273
|
+
source: 'presentation-route-coverage',
|
|
274
|
+
subject: {
|
|
275
|
+
id: trace.pathname,
|
|
276
|
+
kind: 'route',
|
|
277
|
+
},
|
|
278
|
+
}))
|
|
279
|
+
return diagnostics
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!trace.pageHost) {
|
|
283
|
+
diagnostics.push(createDiagnostic({
|
|
284
|
+
id: `route.${trace.route.id}.missing-page-host`,
|
|
285
|
+
message: `Route '${trace.route.id}' resolves to page host '${trace.route.pageHostId}', but the page host is not available.`,
|
|
286
|
+
severity: 'error',
|
|
287
|
+
source: 'presentation-route-coverage',
|
|
288
|
+
subject: {
|
|
289
|
+
id: trace.route.id,
|
|
290
|
+
kind: 'route',
|
|
291
|
+
name: trace.route.label,
|
|
292
|
+
},
|
|
293
|
+
}))
|
|
294
|
+
} else if (!trace.pageHostRenderer?.resolutionSource) {
|
|
295
|
+
diagnostics.push(createDiagnostic({
|
|
296
|
+
category: 'missing-binding',
|
|
297
|
+
details: {
|
|
298
|
+
componentKey: trace.pageHostRenderer?.componentKey ?? null,
|
|
299
|
+
componentRole: trace.pageHostRenderer?.componentRole ?? null,
|
|
300
|
+
rendererKey: trace.pageHostRenderer?.rendererKey ?? null,
|
|
301
|
+
semanticRole: trace.pageHostRenderer?.semanticRole ?? null,
|
|
302
|
+
targetBindingSource: trace.pageHostRenderer?.targetBindingSource ?? null,
|
|
303
|
+
},
|
|
304
|
+
id: `page-host.${trace.pageHost.id}.missing-renderer`,
|
|
305
|
+
interpretation: {
|
|
306
|
+
status: 'unbound',
|
|
307
|
+
target: 'page-host-renderer',
|
|
308
|
+
},
|
|
309
|
+
message: `Page host '${trace.pageHost.id}' has no frontend renderer binding.`,
|
|
310
|
+
severity: 'warning',
|
|
311
|
+
source: 'presentation-route-coverage',
|
|
312
|
+
subject: {
|
|
313
|
+
id: trace.pageHost.id,
|
|
314
|
+
kind: 'page-host',
|
|
315
|
+
},
|
|
316
|
+
suggestedNextStep:
|
|
317
|
+
'Add a page-host renderer binding or project this host through the generic routed-surface interpreter.',
|
|
318
|
+
}))
|
|
319
|
+
} else if (
|
|
320
|
+
trace.pageHostRenderer?.resolutionSource &&
|
|
321
|
+
isPageHostRendererEscapeHatchSource(trace.pageHostRenderer.resolutionSource)
|
|
322
|
+
) {
|
|
323
|
+
diagnostics.push(createDiagnostic({
|
|
324
|
+
category: 'escape-hatch',
|
|
325
|
+
details: {
|
|
326
|
+
componentKey: trace.pageHostRenderer.componentKey,
|
|
327
|
+
componentRole: trace.pageHostRenderer.componentRole,
|
|
328
|
+
rendererKey: trace.pageHostRenderer.rendererKey,
|
|
329
|
+
resolutionSource: trace.pageHostRenderer.resolutionSource,
|
|
330
|
+
semanticRole: trace.pageHostRenderer.semanticRole,
|
|
331
|
+
targetBindingSource: trace.pageHostRenderer.targetBindingSource,
|
|
332
|
+
},
|
|
333
|
+
id: `page-host.${trace.pageHost.id}.${trace.pageHostRenderer.resolutionSource}-escape-hatch`,
|
|
334
|
+
interpretation: {
|
|
335
|
+
status: 'escape-hatch',
|
|
336
|
+
target: 'page-host-renderer',
|
|
337
|
+
},
|
|
338
|
+
message:
|
|
339
|
+
`Page host '${trace.pageHost.id}' rendered through ` +
|
|
340
|
+
`'${trace.pageHostRenderer.resolutionSource}' as an escape hatch.`,
|
|
341
|
+
severity: 'warning',
|
|
342
|
+
source: 'presentation-route-coverage',
|
|
343
|
+
subject: {
|
|
344
|
+
id: trace.pageHost.id,
|
|
345
|
+
kind: 'page-host',
|
|
346
|
+
},
|
|
347
|
+
suggestedNextStep:
|
|
348
|
+
'Move this page host onto ComponentRole, workspace, root-view semantic role, or view kind resolution.',
|
|
349
|
+
}))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (trace.pageHost && trace.pageHostRenderer?.rendererKey) {
|
|
353
|
+
diagnostics.push(createDiagnostic({
|
|
354
|
+
category: 'escape-hatch',
|
|
355
|
+
details: {
|
|
356
|
+
rendererKey: trace.pageHostRenderer.rendererKey,
|
|
357
|
+
resolutionSource: trace.pageHostRenderer.resolutionSource,
|
|
358
|
+
targetBindingSource: trace.pageHostRenderer.targetBindingSource,
|
|
359
|
+
},
|
|
360
|
+
id: `page-host.${trace.pageHost.id}.legacy-renderer-key`,
|
|
361
|
+
interpretation: {
|
|
362
|
+
status: 'escape-hatch',
|
|
363
|
+
target: 'page-host-component-role',
|
|
364
|
+
},
|
|
365
|
+
message:
|
|
366
|
+
`Page host '${trace.pageHost.id}' declares legacy renderer key ` +
|
|
367
|
+
`'${trace.pageHostRenderer.rendererKey}'. Page-host dispatch now uses ` +
|
|
368
|
+
'component roles and semantic target interpretation.',
|
|
369
|
+
severity: 'warning',
|
|
370
|
+
source: 'presentation-route-coverage',
|
|
371
|
+
subject: {
|
|
372
|
+
id: trace.pageHost.id,
|
|
373
|
+
kind: 'page-host',
|
|
374
|
+
},
|
|
375
|
+
suggestedNextStep:
|
|
376
|
+
'Replace the renderer key with a PageHostComponent target binding that declares ComponentRole.',
|
|
377
|
+
}))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return diagnostics
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function isPageHostRendererEscapeHatchSource(source: string) {
|
|
384
|
+
return source === 'component-key' ||
|
|
385
|
+
source === 'page-host-id' ||
|
|
386
|
+
source === 'route-id' ||
|
|
387
|
+
source === 'view-id' ||
|
|
388
|
+
source === 'workspace-id'
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function projectDataSourceQueryLoweringTransformDiagnostics({
|
|
392
|
+
dataSource,
|
|
393
|
+
sourceId,
|
|
394
|
+
targetInterpretation,
|
|
395
|
+
}: {
|
|
396
|
+
readonly dataSource: PresentationModuleDefinition['DataSources'][number]
|
|
397
|
+
readonly sourceId: string
|
|
398
|
+
readonly targetInterpretation?: Pick<PresentationDataSourceTargetInterpretation, 'queryLowering'>
|
|
399
|
+
}): readonly PresentationProjectionDiagnostic[] {
|
|
400
|
+
const usedTransformIds = collectDataSourceQueryLoweringTransformIds(dataSource)
|
|
401
|
+
if (usedTransformIds.length === 0) {
|
|
402
|
+
return []
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const boundTransformIds = new Set(
|
|
406
|
+
Object.keys(targetInterpretation?.queryLowering?.transformsById ?? {}),
|
|
407
|
+
)
|
|
408
|
+
return usedTransformIds
|
|
409
|
+
.filter((transformId) => !boundTransformIds.has(transformId))
|
|
410
|
+
.map((transformId) =>
|
|
411
|
+
createDiagnostic({
|
|
412
|
+
category: 'unbound',
|
|
413
|
+
details: {
|
|
414
|
+
transformId,
|
|
415
|
+
},
|
|
416
|
+
id: `data-source.${dataSource.Id}.query-transform.${transformId}.unbound`,
|
|
417
|
+
interpretation: {
|
|
418
|
+
status: 'unbound',
|
|
419
|
+
target: 'query-lowering-transform',
|
|
420
|
+
},
|
|
421
|
+
message:
|
|
422
|
+
`Data source '${dataSource.Name}' uses query lowering transform ` +
|
|
423
|
+
`'${transformId}', but the active frontend target interpretation does not bind it.`,
|
|
424
|
+
severity: 'warning',
|
|
425
|
+
source: sourceId,
|
|
426
|
+
subject: {
|
|
427
|
+
id: dataSource.Id,
|
|
428
|
+
kind: 'data-source',
|
|
429
|
+
name: dataSource.Name,
|
|
430
|
+
},
|
|
431
|
+
suggestedNextStep:
|
|
432
|
+
'Bind this transform id on the data-source target interpretation or replace it with a projected expression.',
|
|
433
|
+
}),
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function collectDataSourceQueryLoweringTransformIds(
|
|
438
|
+
dataSource: PresentationModuleDefinition['DataSources'][number],
|
|
439
|
+
) {
|
|
440
|
+
const transformIds = new Set<string>()
|
|
441
|
+
for (const endpointBinding of dataSource.Query?.EndpointBindings ?? []) {
|
|
442
|
+
for (const lowering of endpointBinding.Lowerings) {
|
|
443
|
+
for (const fieldBinding of lowering.FieldBindings) {
|
|
444
|
+
const transform = fieldBinding.Transform?.trim()
|
|
445
|
+
if (transform) {
|
|
446
|
+
transformIds.add(transform)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return Array.from(transformIds).sort()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function projectViewRendererCoverageDiagnostics(
|
|
456
|
+
trace: PresentationProjectionTrace,
|
|
457
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
458
|
+
return trace.views.flatMap((view) => {
|
|
459
|
+
if (isViewRendererCoverageOwnedByDocumentWorkspaceHost(trace, view)) {
|
|
460
|
+
return []
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!view.rendererResolved) {
|
|
464
|
+
return [
|
|
465
|
+
createViewDiagnostic({
|
|
466
|
+
category: 'missing-binding',
|
|
467
|
+
details: {
|
|
468
|
+
componentKey: view.componentKey,
|
|
469
|
+
componentRole: view.componentRole,
|
|
470
|
+
semanticRole: view.semanticRole,
|
|
471
|
+
viewKind: view.kind,
|
|
472
|
+
},
|
|
473
|
+
interpretation: {
|
|
474
|
+
status: 'unbound',
|
|
475
|
+
target: 'view-renderer',
|
|
476
|
+
},
|
|
477
|
+
message: `View '${view.name}' has no frontend renderer binding.`,
|
|
478
|
+
reason: 'missing-renderer',
|
|
479
|
+
severity: 'warning',
|
|
480
|
+
source: 'presentation-view-coverage',
|
|
481
|
+
suggestedNextStep:
|
|
482
|
+
'Add a semantic renderer binding or expand the standard view interpreter for this view kind.',
|
|
483
|
+
view,
|
|
484
|
+
}),
|
|
485
|
+
]
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (view.componentKey && view.resolutionSource === 'component-key') {
|
|
489
|
+
return [
|
|
490
|
+
createViewDiagnostic({
|
|
491
|
+
category: 'escape-hatch',
|
|
492
|
+
details: {
|
|
493
|
+
componentKey: view.componentKey,
|
|
494
|
+
componentRole: view.componentRole,
|
|
495
|
+
resolutionSource: view.resolutionSource,
|
|
496
|
+
},
|
|
497
|
+
interpretation: {
|
|
498
|
+
status: 'escape-hatch',
|
|
499
|
+
target: 'view-renderer',
|
|
500
|
+
},
|
|
501
|
+
message:
|
|
502
|
+
`View '${view.name}' rendered through concrete component key ` +
|
|
503
|
+
`'${view.componentKey}' as an escape hatch.`,
|
|
504
|
+
reason: 'component-key-escape-hatch',
|
|
505
|
+
severity: 'warning',
|
|
506
|
+
source: 'presentation-view-coverage',
|
|
507
|
+
suggestedNextStep:
|
|
508
|
+
'Prefer ComponentRole or semantic renderer resolution; keep ComponentKey only for adapter-specific overrides.',
|
|
509
|
+
view,
|
|
510
|
+
}),
|
|
511
|
+
]
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return []
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function isViewRendererCoverageOwnedByDocumentWorkspaceHost(
|
|
519
|
+
trace: PresentationProjectionTrace,
|
|
520
|
+
view: PresentationProjectionTraceView,
|
|
521
|
+
) {
|
|
522
|
+
const pageHostComponentRole = trace.pageHostRenderer?.componentRole
|
|
523
|
+
const isDocumentWorkspaceHost =
|
|
524
|
+
pageHostComponentRole === pageHostComponentRoles.documentWorkspace ||
|
|
525
|
+
Boolean(trace.pageHost?.documentProfileId)
|
|
526
|
+
|
|
527
|
+
if (!isDocumentWorkspaceHost) {
|
|
528
|
+
return false
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
view.semanticRole === 'workspace-view' ||
|
|
533
|
+
view.semanticRole === 'document-view' ||
|
|
534
|
+
isTraceViewKind(view, viewKinds.documentWorkspace, 'DocumentWorkspace') ||
|
|
535
|
+
isTraceViewKind(view, viewKinds.panel, 'Panel')
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isTraceViewKind(
|
|
540
|
+
view: PresentationProjectionTraceView,
|
|
541
|
+
kind: string | number,
|
|
542
|
+
label: string,
|
|
543
|
+
) {
|
|
544
|
+
return view.kind === String(kind) || view.kind === label
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function projectViewModelCoverageDiagnostics(
|
|
548
|
+
module: PresentationProjectionCoverageModule | null,
|
|
549
|
+
trace: PresentationProjectionTrace,
|
|
550
|
+
queryFormStateAdapterIds: readonly string[],
|
|
551
|
+
collectionSelectionStateRuntimeBound: boolean,
|
|
552
|
+
collectionChromeSlotRendererKeys: readonly string[],
|
|
553
|
+
viewChromeSlotRendererKeys: readonly string[],
|
|
554
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
555
|
+
if (!module) {
|
|
556
|
+
return []
|
|
557
|
+
}
|
|
558
|
+
const coverageModule = normalizeCoverageModule(module)
|
|
559
|
+
|
|
560
|
+
return trace.views.flatMap((viewTrace) => {
|
|
561
|
+
const view = findPresentationView<ViewDefinition>(coverageModule, viewTrace.id)
|
|
562
|
+
if (!view) {
|
|
563
|
+
return [
|
|
564
|
+
createViewDiagnostic({
|
|
565
|
+
message: `View '${viewTrace.name}' is in the active trace but is not present in the presentation module.`,
|
|
566
|
+
reason: 'missing-definition',
|
|
567
|
+
severity: 'error',
|
|
568
|
+
source: 'presentation-view-coverage',
|
|
569
|
+
view: viewTrace,
|
|
570
|
+
}),
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return [
|
|
575
|
+
...projectViewChromeCoverageDiagnostics(
|
|
576
|
+
coverageModule,
|
|
577
|
+
view,
|
|
578
|
+
viewTrace,
|
|
579
|
+
viewChromeSlotRendererKeys,
|
|
580
|
+
),
|
|
581
|
+
...projectCollectionCoverageDiagnostics(
|
|
582
|
+
coverageModule,
|
|
583
|
+
view,
|
|
584
|
+
viewTrace,
|
|
585
|
+
collectionSelectionStateRuntimeBound,
|
|
586
|
+
collectionChromeSlotRendererKeys,
|
|
587
|
+
),
|
|
588
|
+
...projectFieldCoverageDiagnostics(coverageModule, view, viewTrace),
|
|
589
|
+
...projectActionCoverageDiagnostics(coverageModule, view, viewTrace),
|
|
590
|
+
...projectInputFormCoverageDiagnostics(
|
|
591
|
+
coverageModule,
|
|
592
|
+
view,
|
|
593
|
+
viewTrace,
|
|
594
|
+
queryFormStateAdapterIds,
|
|
595
|
+
),
|
|
596
|
+
]
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function projectFieldCoverageDiagnostics(
|
|
601
|
+
module: PresentationModuleDefinition,
|
|
602
|
+
view: ViewDefinition,
|
|
603
|
+
viewTrace: PresentationProjectionTraceView,
|
|
604
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
605
|
+
return resolveViewProjectedFieldRefs(view).flatMap(({ fieldId, source }) => {
|
|
606
|
+
const field = findPresentationField(module, fieldId)
|
|
607
|
+
return field
|
|
608
|
+
? []
|
|
609
|
+
: [
|
|
610
|
+
createViewDiagnostic({
|
|
611
|
+
details: { fieldId, source },
|
|
612
|
+
message: `View '${view.Name}' references field '${fieldId}' from ${source}, but the field is not present in the presentation module.`,
|
|
613
|
+
reason: `missing-field.${source}.${fieldId}`,
|
|
614
|
+
severity: 'error',
|
|
615
|
+
source: 'presentation-view-coverage',
|
|
616
|
+
view: viewTrace,
|
|
617
|
+
}),
|
|
618
|
+
]
|
|
619
|
+
})
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function projectViewChromeCoverageDiagnostics(
|
|
623
|
+
module: PresentationModuleDefinition,
|
|
624
|
+
view: ViewDefinition,
|
|
625
|
+
viewTrace: PresentationProjectionTraceView,
|
|
626
|
+
viewChromeSlotRendererKeys: readonly string[],
|
|
627
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
628
|
+
return [
|
|
629
|
+
...(view.Chrome?.Slots ?? []).flatMap((slot) => [
|
|
630
|
+
...projectViewChromeSlotReferenceDiagnostics(module, view, viewTrace, slot),
|
|
631
|
+
...projectViewChromeSlotInterpretationDiagnostics(
|
|
632
|
+
view,
|
|
633
|
+
viewTrace,
|
|
634
|
+
slot,
|
|
635
|
+
viewChromeSlotRendererKeys,
|
|
636
|
+
),
|
|
637
|
+
]),
|
|
638
|
+
...projectPromptStatusChromeCoverageDiagnostics(view, viewTrace),
|
|
639
|
+
...projectPromptStatusMessageContentDiagnostics(view, viewTrace),
|
|
640
|
+
...projectPromptDocumentPreviewContentDiagnostics(view, viewTrace),
|
|
641
|
+
...projectPromptDocumentPreviewBadgeContentDiagnostics(module, view, viewTrace),
|
|
642
|
+
]
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function projectPromptStatusChromeCoverageDiagnostics(
|
|
646
|
+
view: ViewDefinition,
|
|
647
|
+
viewTrace: PresentationProjectionTraceView,
|
|
648
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
649
|
+
const statusMessages = view.PromptStatusMessages ?? []
|
|
650
|
+
if (statusMessages.length === 0) {
|
|
651
|
+
return []
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const statusSlotRegionIds = new Set(
|
|
655
|
+
(view.Chrome?.Slots ?? [])
|
|
656
|
+
.filter((slot) => isViewChromeSlotKind(slot, viewChromeSlotKinds.status))
|
|
657
|
+
.map((slot) => slot.StateId ?? slot.Id),
|
|
658
|
+
)
|
|
659
|
+
const messageRegionIds = Array.from(new Set(
|
|
660
|
+
statusMessages.map((message) => message.Region).filter(Boolean),
|
|
661
|
+
))
|
|
662
|
+
|
|
663
|
+
return messageRegionIds
|
|
664
|
+
.filter((regionId) => !statusSlotRegionIds.has(regionId))
|
|
665
|
+
.map((regionId) =>
|
|
666
|
+
createViewDiagnostic({
|
|
667
|
+
category: 'missing-binding',
|
|
668
|
+
details: { promptStatusRegionId: regionId },
|
|
669
|
+
interpretation: {
|
|
670
|
+
status: 'unbound',
|
|
671
|
+
target: 'view-chrome-status-slot',
|
|
672
|
+
},
|
|
673
|
+
message:
|
|
674
|
+
`Prompt view '${view.Name}' declares status messages for region ` +
|
|
675
|
+
`'${regionId}', but no Status chrome slot consumes that region.`,
|
|
676
|
+
reason: `unbound-prompt-status-region.${regionId}`,
|
|
677
|
+
severity: 'warning',
|
|
678
|
+
source: 'presentation-view-coverage',
|
|
679
|
+
suggestedNextStep:
|
|
680
|
+
'Declare a View.Chrome Status slot whose id or state id matches this prompt status region.',
|
|
681
|
+
view: viewTrace,
|
|
682
|
+
}))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function projectPromptStatusMessageContentDiagnostics(
|
|
686
|
+
view: ViewDefinition,
|
|
687
|
+
viewTrace: PresentationProjectionTraceView,
|
|
688
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
689
|
+
return (view.PromptStatusMessages ?? []).flatMap((message) => {
|
|
690
|
+
const hasLegacyMessage = Boolean(message.Message)
|
|
691
|
+
const hasLegacyTemplate = Boolean(message.MessageTemplate)
|
|
692
|
+
if (!hasLegacyMessage && !hasLegacyTemplate) {
|
|
693
|
+
return []
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const content = message.Content
|
|
697
|
+
if (!content) {
|
|
698
|
+
return [
|
|
699
|
+
createViewDiagnostic({
|
|
700
|
+
category: 'incomplete-projection',
|
|
701
|
+
details: {
|
|
702
|
+
promptStatusMessageId: message.Id,
|
|
703
|
+
promptStatusMessageName: message.Name,
|
|
704
|
+
region: message.Region,
|
|
705
|
+
},
|
|
706
|
+
interpretation: {
|
|
707
|
+
status: 'locally-interpreted',
|
|
708
|
+
target: 'prompt-status-message-content',
|
|
709
|
+
},
|
|
710
|
+
message:
|
|
711
|
+
`Prompt status message '${message.Name}' on view '${view.Name}' still declares ` +
|
|
712
|
+
'legacy message fields without Content; the frontend will use legacy text fallback semantics.',
|
|
713
|
+
reason: `legacy-prompt-status-content.${message.Id}`,
|
|
714
|
+
severity: 'warning',
|
|
715
|
+
source: 'presentation-view-coverage',
|
|
716
|
+
suggestedNextStep:
|
|
717
|
+
'Declare PromptStatusMessage.Content with PresentationContentDefinition and remove legacy Message/MessageTemplate fallback fields.',
|
|
718
|
+
view: viewTrace,
|
|
719
|
+
}),
|
|
720
|
+
]
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const hasContentLabel = Boolean(
|
|
724
|
+
content.Description ??
|
|
725
|
+
content.DescriptionTemplate ??
|
|
726
|
+
content.Title ??
|
|
727
|
+
content.Subtitle,
|
|
728
|
+
)
|
|
729
|
+
return hasContentLabel
|
|
730
|
+
? []
|
|
731
|
+
: [
|
|
732
|
+
createViewDiagnostic({
|
|
733
|
+
category: 'incomplete-projection',
|
|
734
|
+
details: {
|
|
735
|
+
promptStatusMessageId: message.Id,
|
|
736
|
+
promptStatusMessageName: message.Name,
|
|
737
|
+
region: message.Region,
|
|
738
|
+
},
|
|
739
|
+
interpretation: {
|
|
740
|
+
status: 'locally-interpreted',
|
|
741
|
+
target: 'prompt-status-message-content',
|
|
742
|
+
},
|
|
743
|
+
message:
|
|
744
|
+
`Prompt status message '${message.Name}' on view '${view.Name}' declares Content ` +
|
|
745
|
+
'without label content; the frontend will use legacy text fallback semantics.',
|
|
746
|
+
reason: `legacy-prompt-status-content-label.${message.Id}`,
|
|
747
|
+
severity: 'warning',
|
|
748
|
+
source: 'presentation-view-coverage',
|
|
749
|
+
suggestedNextStep:
|
|
750
|
+
'Declare PromptStatusMessage.Content title, subtitle, description, or description template.',
|
|
751
|
+
view: viewTrace,
|
|
752
|
+
}),
|
|
753
|
+
]
|
|
754
|
+
})
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function projectPromptDocumentPreviewContentDiagnostics(
|
|
758
|
+
view: ViewDefinition,
|
|
759
|
+
viewTrace: PresentationProjectionTraceView,
|
|
760
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
761
|
+
const preview = view.PromptDocumentPreview
|
|
762
|
+
if (!preview) {
|
|
763
|
+
return []
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (!preview.Content) {
|
|
767
|
+
return [
|
|
768
|
+
createViewDiagnostic({
|
|
769
|
+
category: 'incomplete-projection',
|
|
770
|
+
details: {
|
|
771
|
+
previewViewId: preview.PreviewViewId,
|
|
772
|
+
workspacePageViewId: preview.WorkspacePageViewId,
|
|
773
|
+
},
|
|
774
|
+
interpretation: {
|
|
775
|
+
status: 'locally-interpreted',
|
|
776
|
+
target: 'prompt-document-preview-content',
|
|
777
|
+
},
|
|
778
|
+
message:
|
|
779
|
+
`Prompt document preview on view '${view.Name}' still declares legacy title/document text ` +
|
|
780
|
+
'without Content; the frontend will use legacy preview content fallback semantics.',
|
|
781
|
+
reason: 'legacy-prompt-document-preview-content',
|
|
782
|
+
severity: 'warning',
|
|
783
|
+
source: 'presentation-view-coverage',
|
|
784
|
+
suggestedNextStep:
|
|
785
|
+
'Declare PromptDocumentPreview.Content with PresentationContentDefinition and remove legacy Title/DocumentText fallback fields.',
|
|
786
|
+
view: viewTrace,
|
|
787
|
+
}),
|
|
788
|
+
]
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
792
|
+
if (!preview.Content.Title) {
|
|
793
|
+
diagnostics.push(
|
|
794
|
+
createViewDiagnostic({
|
|
795
|
+
category: 'incomplete-projection',
|
|
796
|
+
details: {
|
|
797
|
+
previewViewId: preview.PreviewViewId,
|
|
798
|
+
workspacePageViewId: preview.WorkspacePageViewId,
|
|
799
|
+
},
|
|
800
|
+
interpretation: {
|
|
801
|
+
status: 'locally-interpreted',
|
|
802
|
+
target: 'prompt-document-preview-content',
|
|
803
|
+
},
|
|
804
|
+
message:
|
|
805
|
+
`Prompt document preview on view '${view.Name}' declares Content without title content; ` +
|
|
806
|
+
'the frontend will use legacy preview title fallback semantics.',
|
|
807
|
+
reason: 'legacy-prompt-document-preview-title',
|
|
808
|
+
severity: 'warning',
|
|
809
|
+
source: 'presentation-view-coverage',
|
|
810
|
+
suggestedNextStep:
|
|
811
|
+
'Declare PromptDocumentPreview.Content.Title.',
|
|
812
|
+
view: viewTrace,
|
|
813
|
+
}),
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const hasDocumentTextContent = Boolean(
|
|
818
|
+
preview.Content.Description ??
|
|
819
|
+
preview.Content.DescriptionTemplate ??
|
|
820
|
+
preview.Content.Subtitle,
|
|
821
|
+
)
|
|
822
|
+
if (!hasDocumentTextContent) {
|
|
823
|
+
diagnostics.push(
|
|
824
|
+
createViewDiagnostic({
|
|
825
|
+
category: 'incomplete-projection',
|
|
826
|
+
details: {
|
|
827
|
+
previewViewId: preview.PreviewViewId,
|
|
828
|
+
workspacePageViewId: preview.WorkspacePageViewId,
|
|
829
|
+
},
|
|
830
|
+
interpretation: {
|
|
831
|
+
status: 'locally-interpreted',
|
|
832
|
+
target: 'prompt-document-preview-content',
|
|
833
|
+
},
|
|
834
|
+
message:
|
|
835
|
+
`Prompt document preview on view '${view.Name}' declares Content without document text content; ` +
|
|
836
|
+
'the frontend will use legacy preview document text fallback semantics.',
|
|
837
|
+
reason: 'legacy-prompt-document-preview-text',
|
|
838
|
+
severity: 'warning',
|
|
839
|
+
source: 'presentation-view-coverage',
|
|
840
|
+
suggestedNextStep:
|
|
841
|
+
'Declare PromptDocumentPreview.Content.Description, DescriptionTemplate, or Subtitle.',
|
|
842
|
+
view: viewTrace,
|
|
843
|
+
}),
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return diagnostics
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function projectPromptDocumentPreviewBadgeContentDiagnostics(
|
|
851
|
+
module: PresentationModuleDefinition,
|
|
852
|
+
view: ViewDefinition,
|
|
853
|
+
viewTrace: PresentationProjectionTraceView,
|
|
854
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
855
|
+
const preview = view.PromptDocumentPreview
|
|
856
|
+
if (!preview) {
|
|
857
|
+
return []
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return preview.Badges.flatMap((badge) => {
|
|
861
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
862
|
+
const hasLegacyValue = Boolean(badge.Value || badge.ValueTemplate)
|
|
863
|
+
const hasContentLabel = Boolean(
|
|
864
|
+
badge.Content?.Description ??
|
|
865
|
+
badge.Content?.DescriptionTemplate ??
|
|
866
|
+
badge.Content?.Title ??
|
|
867
|
+
badge.Content?.Subtitle,
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
if (!badge.Content) {
|
|
871
|
+
diagnostics.push(createViewDiagnostic({
|
|
872
|
+
category: 'incomplete-projection',
|
|
873
|
+
details: {
|
|
874
|
+
badgeId: badge.Id,
|
|
875
|
+
badgeName: badge.Name,
|
|
876
|
+
previewViewId: preview.PreviewViewId,
|
|
877
|
+
workspacePageViewId: preview.WorkspacePageViewId,
|
|
878
|
+
},
|
|
879
|
+
interpretation: {
|
|
880
|
+
status: 'locally-interpreted',
|
|
881
|
+
target: 'presentation-badge-content',
|
|
882
|
+
},
|
|
883
|
+
message:
|
|
884
|
+
`Prompt document preview badge '${badge.Name}' on view '${view.Name}' does not ` +
|
|
885
|
+
'declare Content; the frontend will use badge value or name fallback semantics.',
|
|
886
|
+
reason: `legacy-prompt-document-preview-badge-content.${badge.Id}`,
|
|
887
|
+
severity: 'warning',
|
|
888
|
+
source: 'presentation-view-coverage',
|
|
889
|
+
suggestedNextStep:
|
|
890
|
+
'Declare PresentationBadge.Content with title, subtitle, description, or description template.',
|
|
891
|
+
view: viewTrace,
|
|
892
|
+
}))
|
|
893
|
+
} else if (hasLegacyValue && !hasContentLabel) {
|
|
894
|
+
diagnostics.push(createViewDiagnostic({
|
|
895
|
+
category: 'incomplete-projection',
|
|
896
|
+
details: {
|
|
897
|
+
badgeId: badge.Id,
|
|
898
|
+
badgeName: badge.Name,
|
|
899
|
+
previewViewId: preview.PreviewViewId,
|
|
900
|
+
workspacePageViewId: preview.WorkspacePageViewId,
|
|
901
|
+
},
|
|
902
|
+
interpretation: {
|
|
903
|
+
status: 'locally-interpreted',
|
|
904
|
+
target: 'presentation-badge-content',
|
|
905
|
+
},
|
|
906
|
+
message:
|
|
907
|
+
`Prompt document preview badge '${badge.Name}' on view '${view.Name}' declares ` +
|
|
908
|
+
'Content without label content; the frontend will use badge value fallback semantics.',
|
|
909
|
+
reason: `legacy-prompt-document-preview-badge-content-label.${badge.Id}`,
|
|
910
|
+
severity: 'warning',
|
|
911
|
+
source: 'presentation-view-coverage',
|
|
912
|
+
suggestedNextStep:
|
|
913
|
+
'Declare PresentationBadge.Content title, subtitle, description, or description template.',
|
|
914
|
+
view: viewTrace,
|
|
915
|
+
}))
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (badge.FieldId && !findPresentationField(module, badge.FieldId)) {
|
|
919
|
+
diagnostics.push(createViewDiagnostic({
|
|
920
|
+
category: 'missing-definition',
|
|
921
|
+
details: {
|
|
922
|
+
badgeId: badge.Id,
|
|
923
|
+
badgeName: badge.Name,
|
|
924
|
+
fieldId: badge.FieldId,
|
|
925
|
+
previewViewId: preview.PreviewViewId,
|
|
926
|
+
workspacePageViewId: preview.WorkspacePageViewId,
|
|
927
|
+
},
|
|
928
|
+
interpretation: {
|
|
929
|
+
status: 'unbound',
|
|
930
|
+
target: 'presentation-field',
|
|
931
|
+
},
|
|
932
|
+
message:
|
|
933
|
+
`Prompt document preview badge '${badge.Name}' on view '${view.Name}' references ` +
|
|
934
|
+
`field '${badge.FieldId}', but the field is not present in the presentation module.`,
|
|
935
|
+
reason: `missing-prompt-document-preview-badge-field.${badge.Id}.${badge.FieldId}`,
|
|
936
|
+
severity: 'error',
|
|
937
|
+
source: 'presentation-view-coverage',
|
|
938
|
+
view: viewTrace,
|
|
939
|
+
}))
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (badge.Tone && !isSupportedPresentationBadgeTone(badge.Tone)) {
|
|
943
|
+
diagnostics.push(createViewDiagnostic({
|
|
944
|
+
category: 'unsupported',
|
|
945
|
+
details: {
|
|
946
|
+
badgeId: badge.Id,
|
|
947
|
+
badgeName: badge.Name,
|
|
948
|
+
supportedTones: presentationBadgeToneValues,
|
|
949
|
+
tone: badge.Tone,
|
|
950
|
+
},
|
|
951
|
+
interpretation: {
|
|
952
|
+
status: 'unsupported',
|
|
953
|
+
target: 'presentation-badge-tone',
|
|
954
|
+
},
|
|
955
|
+
message:
|
|
956
|
+
`Prompt document preview badge '${badge.Name}' on view '${view.Name}' declares ` +
|
|
957
|
+
`tone '${badge.Tone}', but the active frontend badge design interpreter does not ` +
|
|
958
|
+
'declare that tone.',
|
|
959
|
+
reason: `unsupported-prompt-document-preview-badge-tone.${badge.Id}.${badge.Tone}`,
|
|
960
|
+
severity: 'warning',
|
|
961
|
+
source: 'presentation-view-coverage',
|
|
962
|
+
suggestedNextStep:
|
|
963
|
+
'Use a standard presentation badge tone or extend the design-system badge tone interpreter.',
|
|
964
|
+
view: viewTrace,
|
|
965
|
+
}))
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return diagnostics
|
|
969
|
+
})
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const presentationBadgeToneValues = [
|
|
973
|
+
'accent',
|
|
974
|
+
'danger',
|
|
975
|
+
'info',
|
|
976
|
+
'muted',
|
|
977
|
+
'neutral',
|
|
978
|
+
'success',
|
|
979
|
+
'warning',
|
|
980
|
+
] as const
|
|
981
|
+
|
|
982
|
+
const presentationBadgeTones = new Set<string>(presentationBadgeToneValues)
|
|
983
|
+
|
|
984
|
+
function isSupportedPresentationBadgeTone(tone: string) {
|
|
985
|
+
return presentationBadgeTones.has(tone.trim().toLowerCase())
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function projectViewChromeSlotReferenceDiagnostics(
|
|
989
|
+
module: PresentationModuleDefinition,
|
|
990
|
+
view: ViewDefinition,
|
|
991
|
+
viewTrace: PresentationProjectionTraceView,
|
|
992
|
+
slot: ViewChromeSlotDefinition,
|
|
993
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
994
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
995
|
+
|
|
996
|
+
if (
|
|
997
|
+
isViewChromeSlotKind(slot, viewChromeSlotKinds.badgeStrip) &&
|
|
998
|
+
slot.FieldIds.length > 0 &&
|
|
999
|
+
(slot.Badges?.length ?? 0) === 0
|
|
1000
|
+
) {
|
|
1001
|
+
diagnostics.push(createViewChromeSlotDiagnostic({
|
|
1002
|
+
category: 'incomplete-projection',
|
|
1003
|
+
details: {
|
|
1004
|
+
fieldIds: slot.FieldIds,
|
|
1005
|
+
},
|
|
1006
|
+
interpretation: {
|
|
1007
|
+
status: 'locally-interpreted',
|
|
1008
|
+
target: 'view-chrome-badge-strip-field-ids',
|
|
1009
|
+
},
|
|
1010
|
+
message:
|
|
1011
|
+
`Badge strip slot '${slot.Id}' on '${view.Name}' still declares FieldIds without ` +
|
|
1012
|
+
'first-class badge definitions; the frontend will synthesize field-backed badges.',
|
|
1013
|
+
reason: `legacy-badge-strip-field-ids.${slot.Id}`,
|
|
1014
|
+
severity: 'warning',
|
|
1015
|
+
slot,
|
|
1016
|
+
suggestedNextStep:
|
|
1017
|
+
'Declare ViewChromeSlot.Badges with PresentationBadgeDefinition entries and remove BadgeStrip FieldIds.',
|
|
1018
|
+
view: viewTrace,
|
|
1019
|
+
}))
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
for (const fieldId of slot.FieldIds) {
|
|
1023
|
+
if (!findPresentationField(module, fieldId)) {
|
|
1024
|
+
diagnostics.push(createViewChromeSlotDiagnostic({
|
|
1025
|
+
message:
|
|
1026
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' references ` +
|
|
1027
|
+
`field '${fieldId}', but the field is not present in the presentation module.`,
|
|
1028
|
+
reason: `missing-view-chrome-slot-field.${slot.Id}.${fieldId}`,
|
|
1029
|
+
severity: 'error',
|
|
1030
|
+
slot,
|
|
1031
|
+
view: viewTrace,
|
|
1032
|
+
}))
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
for (const badge of slot.Badges ?? []) {
|
|
1037
|
+
if (badge.FieldId && !findPresentationField(module, badge.FieldId)) {
|
|
1038
|
+
diagnostics.push(createViewChromeSlotDiagnostic({
|
|
1039
|
+
category: 'missing-definition',
|
|
1040
|
+
details: {
|
|
1041
|
+
badgeId: badge.Id,
|
|
1042
|
+
badgeName: badge.Name,
|
|
1043
|
+
fieldId: badge.FieldId,
|
|
1044
|
+
},
|
|
1045
|
+
interpretation: {
|
|
1046
|
+
status: 'unbound',
|
|
1047
|
+
target: 'presentation-field',
|
|
1048
|
+
},
|
|
1049
|
+
message:
|
|
1050
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' declares badge ` +
|
|
1051
|
+
`'${badge.Name}' for field '${badge.FieldId}', but the field is not present ` +
|
|
1052
|
+
'in the presentation module.',
|
|
1053
|
+
reason: `missing-view-chrome-slot-badge-field.${slot.Id}.${badge.Id}.${badge.FieldId}`,
|
|
1054
|
+
severity: 'error',
|
|
1055
|
+
slot,
|
|
1056
|
+
view: viewTrace,
|
|
1057
|
+
}))
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (badge.Tone && !isSupportedPresentationBadgeTone(badge.Tone)) {
|
|
1061
|
+
diagnostics.push(createViewChromeSlotDiagnostic({
|
|
1062
|
+
category: 'unsupported',
|
|
1063
|
+
details: {
|
|
1064
|
+
badgeId: badge.Id,
|
|
1065
|
+
badgeName: badge.Name,
|
|
1066
|
+
supportedTones: presentationBadgeToneValues,
|
|
1067
|
+
tone: badge.Tone,
|
|
1068
|
+
},
|
|
1069
|
+
interpretation: {
|
|
1070
|
+
status: 'unsupported',
|
|
1071
|
+
target: 'presentation-badge-tone',
|
|
1072
|
+
},
|
|
1073
|
+
message:
|
|
1074
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' declares badge ` +
|
|
1075
|
+
`'${badge.Name}' with tone '${badge.Tone}', but the active frontend badge ` +
|
|
1076
|
+
'design interpreter does not declare that tone.',
|
|
1077
|
+
reason: `unsupported-view-chrome-slot-badge-tone.${slot.Id}.${badge.Id}.${badge.Tone}`,
|
|
1078
|
+
severity: 'warning',
|
|
1079
|
+
slot,
|
|
1080
|
+
suggestedNextStep:
|
|
1081
|
+
'Use a standard presentation badge tone or extend the design-system badge tone interpreter.',
|
|
1082
|
+
view: viewTrace,
|
|
1083
|
+
}))
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
for (const actionPlacement of slot.Actions) {
|
|
1088
|
+
if (!findPresentationAction(module, actionPlacement.ActionId)) {
|
|
1089
|
+
diagnostics.push(createViewChromeSlotDiagnostic({
|
|
1090
|
+
message:
|
|
1091
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' references ` +
|
|
1092
|
+
`action '${actionPlacement.ActionId}', but the action is not present in the presentation module.`,
|
|
1093
|
+
reason: `missing-view-chrome-slot-action.${slot.Id}.${actionPlacement.ActionId}`,
|
|
1094
|
+
severity: 'error',
|
|
1095
|
+
slot,
|
|
1096
|
+
view: viewTrace,
|
|
1097
|
+
}))
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
for (const viewId of slot.ViewIds) {
|
|
1102
|
+
if (!findPresentationView(module, viewId)) {
|
|
1103
|
+
diagnostics.push(createViewChromeSlotDiagnostic({
|
|
1104
|
+
message:
|
|
1105
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' references ` +
|
|
1106
|
+
`view '${viewId}', but the view is not present in the presentation module.`,
|
|
1107
|
+
reason: `missing-view-chrome-slot-view.${slot.Id}.${viewId}`,
|
|
1108
|
+
severity: 'error',
|
|
1109
|
+
slot,
|
|
1110
|
+
view: viewTrace,
|
|
1111
|
+
}))
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (slot.StateId && !view.State.some((state) => state.Id === slot.StateId)) {
|
|
1116
|
+
diagnostics.push(createViewChromeSlotDiagnostic({
|
|
1117
|
+
message:
|
|
1118
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' references ` +
|
|
1119
|
+
`state '${slot.StateId}', but the state is not declared by the view.`,
|
|
1120
|
+
reason: `missing-view-chrome-slot-state.${slot.Id}.${slot.StateId}`,
|
|
1121
|
+
severity: 'error',
|
|
1122
|
+
slot,
|
|
1123
|
+
view: viewTrace,
|
|
1124
|
+
}))
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return diagnostics
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function projectViewChromeSlotInterpretationDiagnostics(
|
|
1131
|
+
view: ViewDefinition,
|
|
1132
|
+
viewTrace: PresentationProjectionTraceView,
|
|
1133
|
+
slot: ViewChromeSlotDefinition,
|
|
1134
|
+
viewChromeSlotRendererKeys: readonly string[],
|
|
1135
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
1136
|
+
const candidateRendererKeys = getViewChromeSlotRendererCandidateKeys(slot)
|
|
1137
|
+
if (hasViewChromeSlotRendererBinding(viewChromeSlotRendererKeys, slot)) {
|
|
1138
|
+
return [
|
|
1139
|
+
createViewChromeSlotDiagnostic({
|
|
1140
|
+
category: 'local-interpretation',
|
|
1141
|
+
details: {
|
|
1142
|
+
candidateRendererKeys,
|
|
1143
|
+
slotId: slot.Id,
|
|
1144
|
+
slotKind: slot.Kind,
|
|
1145
|
+
slotPlacement: slot.Placement,
|
|
1146
|
+
},
|
|
1147
|
+
interpretation: {
|
|
1148
|
+
status: 'bound',
|
|
1149
|
+
target: 'view-chrome-slot-renderer-registry',
|
|
1150
|
+
},
|
|
1151
|
+
message:
|
|
1152
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' is bound ` +
|
|
1153
|
+
'by the view chrome slot renderer registry.',
|
|
1154
|
+
reason: `bound-view-chrome-slot-renderer.${slot.Id}`,
|
|
1155
|
+
severity: 'info',
|
|
1156
|
+
slot,
|
|
1157
|
+
view: viewTrace,
|
|
1158
|
+
}),
|
|
1159
|
+
]
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (isViewChromeSlotKind(slot, viewChromeSlotKinds.custom)) {
|
|
1163
|
+
return [
|
|
1164
|
+
createViewChromeSlotDiagnostic({
|
|
1165
|
+
category: 'missing-binding',
|
|
1166
|
+
details: {
|
|
1167
|
+
candidateRendererKeys,
|
|
1168
|
+
slotId: slot.Id,
|
|
1169
|
+
slotKind: slot.Kind,
|
|
1170
|
+
slotPlacement: slot.Placement,
|
|
1171
|
+
},
|
|
1172
|
+
interpretation: {
|
|
1173
|
+
status: 'unbound',
|
|
1174
|
+
target: 'view-chrome-slot-renderer-registry',
|
|
1175
|
+
},
|
|
1176
|
+
message:
|
|
1177
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' is Custom, ` +
|
|
1178
|
+
'but no frontend view chrome-slot interpreter is registered for it.',
|
|
1179
|
+
reason: `unbound-view-chrome-custom-slot.${slot.Id}`,
|
|
1180
|
+
severity: 'warning',
|
|
1181
|
+
slot,
|
|
1182
|
+
suggestedNextStep:
|
|
1183
|
+
'Register a view chrome-slot renderer for this custom slot or model it with a first-class slot kind.',
|
|
1184
|
+
view: viewTrace,
|
|
1185
|
+
}),
|
|
1186
|
+
]
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (isViewChromeSlotRendererExpected(slot)) {
|
|
1190
|
+
return [
|
|
1191
|
+
createViewChromeSlotDiagnostic({
|
|
1192
|
+
category: 'missing-binding',
|
|
1193
|
+
details: {
|
|
1194
|
+
candidateRendererKeys,
|
|
1195
|
+
slotId: slot.Id,
|
|
1196
|
+
slotKind: slot.Kind,
|
|
1197
|
+
slotPlacement: slot.Placement,
|
|
1198
|
+
},
|
|
1199
|
+
interpretation: {
|
|
1200
|
+
status: 'unbound',
|
|
1201
|
+
target: 'view-chrome-slot-renderer-registry',
|
|
1202
|
+
},
|
|
1203
|
+
message:
|
|
1204
|
+
`View chrome slot '${slot.Id}' on '${view.Name}' has no ` +
|
|
1205
|
+
'registered frontend renderer for its kind and placement.',
|
|
1206
|
+
reason: `unbound-view-chrome-slot-renderer.${slot.Id}`,
|
|
1207
|
+
severity: 'warning',
|
|
1208
|
+
slot,
|
|
1209
|
+
suggestedNextStep:
|
|
1210
|
+
'Register a view chrome-slot renderer or add a generic interpreter for this slot kind.',
|
|
1211
|
+
view: viewTrace,
|
|
1212
|
+
}),
|
|
1213
|
+
]
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return []
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function isViewChromeSlotRendererExpected(
|
|
1220
|
+
slot: ViewChromeSlotDefinition,
|
|
1221
|
+
) {
|
|
1222
|
+
return isViewChromeSlotKind(slot, viewChromeSlotKinds.actions) ||
|
|
1223
|
+
isViewChromeSlotKind(slot, viewChromeSlotKinds.badgeStrip) ||
|
|
1224
|
+
isViewChromeSlotKind(slot, viewChromeSlotKinds.metricStrip) ||
|
|
1225
|
+
isViewChromeSlotKind(slot, viewChromeSlotKinds.viewSwitch) ||
|
|
1226
|
+
isViewChromeSlotKind(slot, viewChromeSlotKinds.layoutSwitch) ||
|
|
1227
|
+
isViewChromeSlotKind(slot, viewChromeSlotKinds.headingTrailing) ||
|
|
1228
|
+
isViewChromeSlotKind(slot, viewChromeSlotKinds.status)
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function createViewChromeSlotDiagnostic({
|
|
1232
|
+
category,
|
|
1233
|
+
details,
|
|
1234
|
+
interpretation,
|
|
1235
|
+
message,
|
|
1236
|
+
reason,
|
|
1237
|
+
severity,
|
|
1238
|
+
slot,
|
|
1239
|
+
suggestedNextStep,
|
|
1240
|
+
view,
|
|
1241
|
+
}: {
|
|
1242
|
+
readonly category?: PresentationProjectionDiagnostic['category']
|
|
1243
|
+
readonly details?: Readonly<Record<string, unknown>>
|
|
1244
|
+
readonly interpretation?: PresentationProjectionDiagnostic['interpretation']
|
|
1245
|
+
readonly message: string
|
|
1246
|
+
readonly reason: string
|
|
1247
|
+
readonly severity: PresentationProjectionDiagnostic['severity']
|
|
1248
|
+
readonly slot: ViewChromeSlotDefinition
|
|
1249
|
+
readonly suggestedNextStep?: string
|
|
1250
|
+
readonly view: PresentationProjectionTraceView
|
|
1251
|
+
}) {
|
|
1252
|
+
return createViewDiagnostic({
|
|
1253
|
+
category,
|
|
1254
|
+
details: {
|
|
1255
|
+
viewChromeSlotId: slot.Id,
|
|
1256
|
+
viewChromeSlotKind: slot.Kind,
|
|
1257
|
+
viewChromeSlotPlacement: slot.Placement,
|
|
1258
|
+
...details,
|
|
1259
|
+
},
|
|
1260
|
+
interpretation,
|
|
1261
|
+
message,
|
|
1262
|
+
reason,
|
|
1263
|
+
severity,
|
|
1264
|
+
source: 'presentation-view-chrome-coverage',
|
|
1265
|
+
suggestedNextStep,
|
|
1266
|
+
view,
|
|
1267
|
+
})
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function projectCollectionCoverageDiagnostics(
|
|
1271
|
+
module: PresentationModuleDefinition,
|
|
1272
|
+
view: ViewDefinition,
|
|
1273
|
+
viewTrace: PresentationProjectionTraceView,
|
|
1274
|
+
collectionSelectionStateRuntimeBound: boolean,
|
|
1275
|
+
collectionChromeSlotRendererKeys: readonly string[],
|
|
1276
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
1277
|
+
const collection = view.Collection
|
|
1278
|
+
if (!collection) {
|
|
1279
|
+
return []
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
1283
|
+
const bodySlot = resolveCollectionChromeBodySlot(collection)
|
|
1284
|
+
const detailSlot = resolveCollectionChromeDetailSlot(collection)
|
|
1285
|
+
const rowIdentityPath = bodySlot?.RowIdentityPath ?? null
|
|
1286
|
+
const selectionMode = bodySlot?.SelectionMode ?? collectionSelectionModes.none
|
|
1287
|
+
const selectionStateId = bodySlot?.StateId ?? null
|
|
1288
|
+
const rowActions = resolveCollectionChromeRowActions(collection)
|
|
1289
|
+
const selectionActions = resolveCollectionChromeSelectionActions(collection)
|
|
1290
|
+
const projectedActions = getPresentationViewProjectedActions(view)
|
|
1291
|
+
|
|
1292
|
+
diagnostics.push(
|
|
1293
|
+
...projectCollectionOuterViewDuplicationDiagnostics(view, viewTrace),
|
|
1294
|
+
...projectProjectedCollectionActionContextDiagnostics({
|
|
1295
|
+
bodySlot,
|
|
1296
|
+
collectionChromeSlotRendererKeys,
|
|
1297
|
+
projectedActions,
|
|
1298
|
+
selectionMode,
|
|
1299
|
+
selectionStateId,
|
|
1300
|
+
view,
|
|
1301
|
+
viewTrace,
|
|
1302
|
+
}),
|
|
1303
|
+
...projectCollectionChromeCoverageDiagnostics(
|
|
1304
|
+
module,
|
|
1305
|
+
view,
|
|
1306
|
+
viewTrace,
|
|
1307
|
+
collectionChromeSlotRendererKeys,
|
|
1308
|
+
),
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
if (bodySlot || detailSlot) {
|
|
1312
|
+
const isSelectionEnabled = isCollectionSelectionEnabled(selectionMode)
|
|
1313
|
+
if (isSelectionEnabled) {
|
|
1314
|
+
if (!rowIdentityPath) {
|
|
1315
|
+
diagnostics.push(createViewDiagnostic({
|
|
1316
|
+
details: {
|
|
1317
|
+
bodySlotId: bodySlot?.Id ?? null,
|
|
1318
|
+
selectionMode,
|
|
1319
|
+
selectionStateId,
|
|
1320
|
+
},
|
|
1321
|
+
message:
|
|
1322
|
+
`Collection view '${view.Name}' enables row selection, ` +
|
|
1323
|
+
'but its Body chrome slot does not declare a row identity path.',
|
|
1324
|
+
reason: `missing-collection-row-identity.${view.Id}`,
|
|
1325
|
+
severity: 'error',
|
|
1326
|
+
source: 'presentation-collection-coverage',
|
|
1327
|
+
view: viewTrace,
|
|
1328
|
+
}))
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (!selectionStateId) {
|
|
1332
|
+
diagnostics.push(createViewDiagnostic({
|
|
1333
|
+
details: {
|
|
1334
|
+
bodySlotId: bodySlot?.Id ?? null,
|
|
1335
|
+
selectionMode,
|
|
1336
|
+
},
|
|
1337
|
+
interpretation: {
|
|
1338
|
+
status: 'unbound',
|
|
1339
|
+
target: 'collection-selection-state',
|
|
1340
|
+
},
|
|
1341
|
+
message:
|
|
1342
|
+
`Collection view '${view.Name}' enables row selection, ` +
|
|
1343
|
+
'but its Body chrome slot does not declare a selection state id.',
|
|
1344
|
+
reason: `missing-collection-selection-state-id.${view.Id}`,
|
|
1345
|
+
severity: 'warning',
|
|
1346
|
+
source: 'presentation-collection-coverage',
|
|
1347
|
+
suggestedNextStep:
|
|
1348
|
+
'Declare StateId on the collection Body chrome slot so frontend targets can bind the selected row identities.',
|
|
1349
|
+
view: viewTrace,
|
|
1350
|
+
}))
|
|
1351
|
+
} else if (!collectionSelectionStateRuntimeBound) {
|
|
1352
|
+
diagnostics.push(createViewDiagnostic({
|
|
1353
|
+
category: 'missing-binding',
|
|
1354
|
+
details: {
|
|
1355
|
+
bodySlotId: bodySlot?.Id ?? null,
|
|
1356
|
+
selectionMode,
|
|
1357
|
+
selectionStateId,
|
|
1358
|
+
},
|
|
1359
|
+
interpretation: {
|
|
1360
|
+
status: 'unbound',
|
|
1361
|
+
target: 'collection-selection-state-runtime',
|
|
1362
|
+
},
|
|
1363
|
+
message:
|
|
1364
|
+
`Collection view '${view.Name}' declares selection state ` +
|
|
1365
|
+
`'${selectionStateId}', but no frontend selection-state runtime is bound.`,
|
|
1366
|
+
reason: `missing-collection-selection-state-runtime.${selectionStateId}`,
|
|
1367
|
+
severity: 'warning',
|
|
1368
|
+
source: 'presentation-collection-coverage',
|
|
1369
|
+
suggestedNextStep:
|
|
1370
|
+
'Install a generic collection selection-state runtime or bind this state id through a target adapter.',
|
|
1371
|
+
view: viewTrace,
|
|
1372
|
+
}))
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const activatedRowActionId = bodySlot?.ActivatedRowActionId ?? null
|
|
1377
|
+
const activatedRowAction = activatedRowActionId
|
|
1378
|
+
? rowActions.find((rowAction) =>
|
|
1379
|
+
rowAction.Id === activatedRowActionId ||
|
|
1380
|
+
rowAction.ActionId === activatedRowActionId)
|
|
1381
|
+
: rowActions.find(isPrimaryCollectionRowAction)
|
|
1382
|
+
if (activatedRowActionId && !activatedRowAction) {
|
|
1383
|
+
diagnostics.push(createViewDiagnostic({
|
|
1384
|
+
details: { activatedRowActionId },
|
|
1385
|
+
message:
|
|
1386
|
+
`Collection view '${view.Name}' declares activated row action ` +
|
|
1387
|
+
`'${activatedRowActionId}', but no row action with that id or action id exists.`,
|
|
1388
|
+
reason: `missing-activated-collection-row-action.${activatedRowActionId}`,
|
|
1389
|
+
severity: 'error',
|
|
1390
|
+
source: 'presentation-collection-coverage',
|
|
1391
|
+
view: viewTrace,
|
|
1392
|
+
}))
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
if (bodySlot?.ActivateOnRowClick && !activatedRowAction) {
|
|
1396
|
+
diagnostics.push(createViewDiagnostic({
|
|
1397
|
+
category: 'missing-binding',
|
|
1398
|
+
details: { activatedRowActionId: activatedRowActionId ?? null },
|
|
1399
|
+
interpretation: {
|
|
1400
|
+
status: 'unbound',
|
|
1401
|
+
target: 'collection-body-row-activation',
|
|
1402
|
+
},
|
|
1403
|
+
message:
|
|
1404
|
+
`Collection view '${view.Name}' enables row activation, ` +
|
|
1405
|
+
'but no activated or primary row action can be resolved from the Body chrome slot.',
|
|
1406
|
+
reason: `missing-row-click-activation-action.${view.Id}`,
|
|
1407
|
+
severity: 'error',
|
|
1408
|
+
source: 'presentation-collection-coverage',
|
|
1409
|
+
view: viewTrace,
|
|
1410
|
+
}))
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (bodySlot?.SelectOnRowClick && !isSelectionEnabled) {
|
|
1414
|
+
diagnostics.push(createViewDiagnostic({
|
|
1415
|
+
category: 'missing-binding',
|
|
1416
|
+
details: {
|
|
1417
|
+
bodySlotId: bodySlot.Id,
|
|
1418
|
+
selectionMode,
|
|
1419
|
+
selectOnRowClick: bodySlot.SelectOnRowClick,
|
|
1420
|
+
},
|
|
1421
|
+
interpretation: {
|
|
1422
|
+
status: 'unbound',
|
|
1423
|
+
target: 'collection-row-click-selection',
|
|
1424
|
+
},
|
|
1425
|
+
message:
|
|
1426
|
+
`Collection view '${view.Name}' enables row-click selection, ` +
|
|
1427
|
+
'but row selection is disabled.',
|
|
1428
|
+
reason: `row-click-selection-without-selection.${view.Id}`,
|
|
1429
|
+
severity: 'warning',
|
|
1430
|
+
source: 'presentation-collection-coverage',
|
|
1431
|
+
suggestedNextStep:
|
|
1432
|
+
'Enable single or multiple selection, or disable SelectOnRowClick.',
|
|
1433
|
+
view: viewTrace,
|
|
1434
|
+
}))
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if (detailSlot?.DetailViewId) {
|
|
1438
|
+
if (isCollectionDetailActivationUnsupported(detailSlot.DetailActivation)) {
|
|
1439
|
+
diagnostics.push(createViewDiagnostic({
|
|
1440
|
+
category: 'missing-binding',
|
|
1441
|
+
details: {
|
|
1442
|
+
detailActivation: detailSlot.DetailActivation,
|
|
1443
|
+
detailSlotId: detailSlot.Id,
|
|
1444
|
+
detailViewId: detailSlot.DetailViewId,
|
|
1445
|
+
},
|
|
1446
|
+
interpretation: {
|
|
1447
|
+
status: 'unbound',
|
|
1448
|
+
target: 'collection-detail-activation',
|
|
1449
|
+
},
|
|
1450
|
+
message:
|
|
1451
|
+
`Collection view '${view.Name}' declares detail activation ` +
|
|
1452
|
+
`'${detailSlot.DetailActivation}', but the frontend interpreter currently binds selection and row-activation detail context only.`,
|
|
1453
|
+
reason: `unsupported-collection-detail-activation.${detailSlot.DetailViewId}`,
|
|
1454
|
+
severity: 'warning',
|
|
1455
|
+
source: 'presentation-collection-coverage',
|
|
1456
|
+
suggestedNextStep:
|
|
1457
|
+
'Use Selection or RowActivation, or add a frontend interpretation for this activation mode.',
|
|
1458
|
+
view: viewTrace,
|
|
1459
|
+
}))
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (
|
|
1463
|
+
isCollectionDetailActivationSelection(detailSlot.DetailActivation) &&
|
|
1464
|
+
!bodySlot?.SelectOnRowClick
|
|
1465
|
+
) {
|
|
1466
|
+
diagnostics.push(createViewDiagnostic({
|
|
1467
|
+
category: 'missing-binding',
|
|
1468
|
+
details: {
|
|
1469
|
+
bodySlotId: bodySlot?.Id ?? null,
|
|
1470
|
+
detailActivation: detailSlot.DetailActivation,
|
|
1471
|
+
detailSlotId: detailSlot.Id,
|
|
1472
|
+
detailViewId: detailSlot.DetailViewId,
|
|
1473
|
+
selectOnRowClick: bodySlot?.SelectOnRowClick ?? false,
|
|
1474
|
+
},
|
|
1475
|
+
interpretation: {
|
|
1476
|
+
status: 'unbound',
|
|
1477
|
+
target: 'collection-selection-detail-context',
|
|
1478
|
+
},
|
|
1479
|
+
message:
|
|
1480
|
+
`Collection view '${view.Name}' renders detail from selection, ` +
|
|
1481
|
+
'but row-click selection is disabled.',
|
|
1482
|
+
reason: `selection-detail-without-row-click-selection.${detailSlot.DetailViewId}`,
|
|
1483
|
+
severity: 'warning',
|
|
1484
|
+
source: 'presentation-collection-coverage',
|
|
1485
|
+
suggestedNextStep:
|
|
1486
|
+
'Enable SelectOnRowClick for selection-driven detail previews, or change the detail activation mode.',
|
|
1487
|
+
view: viewTrace,
|
|
1488
|
+
}))
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (
|
|
1492
|
+
isCollectionDetailActivationSelection(detailSlot.DetailActivation) &&
|
|
1493
|
+
bodySlot?.SelectOnRowClick &&
|
|
1494
|
+
bodySlot.ActivateOnRowClick
|
|
1495
|
+
) {
|
|
1496
|
+
diagnostics.push(createViewDiagnostic({
|
|
1497
|
+
category: 'unsupported',
|
|
1498
|
+
details: {
|
|
1499
|
+
bodySlotId: bodySlot.Id,
|
|
1500
|
+
detailActivation: detailSlot.DetailActivation,
|
|
1501
|
+
detailSlotId: detailSlot.Id,
|
|
1502
|
+
detailViewId: detailSlot.DetailViewId,
|
|
1503
|
+
},
|
|
1504
|
+
interpretation: {
|
|
1505
|
+
status: 'unsupported',
|
|
1506
|
+
target: 'collection-row-click-interaction',
|
|
1507
|
+
},
|
|
1508
|
+
message:
|
|
1509
|
+
`Collection view '${view.Name}' both selects and activates rows on click; ` +
|
|
1510
|
+
'selection-driven detail previews may be bypassed by navigation.',
|
|
1511
|
+
reason: `selection-detail-and-row-click-activation.${detailSlot.DetailViewId}`,
|
|
1512
|
+
severity: 'warning',
|
|
1513
|
+
source: 'presentation-collection-coverage',
|
|
1514
|
+
suggestedNextStep:
|
|
1515
|
+
'Use SelectOnRowClick for preview-first rows and expose navigation through row actions.',
|
|
1516
|
+
view: viewTrace,
|
|
1517
|
+
}))
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (!findPresentationView(module, detailSlot.DetailViewId)) {
|
|
1521
|
+
diagnostics.push(createViewDiagnostic({
|
|
1522
|
+
details: {
|
|
1523
|
+
detailSlotId: detailSlot.Id,
|
|
1524
|
+
detailViewId: detailSlot.DetailViewId,
|
|
1525
|
+
},
|
|
1526
|
+
message:
|
|
1527
|
+
`Collection view '${view.Name}' references detail view ` +
|
|
1528
|
+
`'${detailSlot.DetailViewId}', but that view is not present in the presentation module.`,
|
|
1529
|
+
reason: `missing-collection-detail-view.${detailSlot.DetailViewId}`,
|
|
1530
|
+
severity: 'error',
|
|
1531
|
+
source: 'presentation-collection-coverage',
|
|
1532
|
+
view: viewTrace,
|
|
1533
|
+
}))
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (!isSelectionEnabled) {
|
|
1537
|
+
diagnostics.push(createViewDiagnostic({
|
|
1538
|
+
category: 'missing-binding',
|
|
1539
|
+
details: {
|
|
1540
|
+
detailSlotId: detailSlot.Id,
|
|
1541
|
+
detailViewId: detailSlot.DetailViewId,
|
|
1542
|
+
},
|
|
1543
|
+
interpretation: {
|
|
1544
|
+
status: 'unbound',
|
|
1545
|
+
target: 'collection-detail-selection-context',
|
|
1546
|
+
},
|
|
1547
|
+
message:
|
|
1548
|
+
`Collection view '${view.Name}' declares detail view ` +
|
|
1549
|
+
`'${detailSlot.DetailViewId}', but row selection is not enabled for the collection.`,
|
|
1550
|
+
reason: `missing-collection-detail-selection-context.${detailSlot.DetailViewId}`,
|
|
1551
|
+
severity: 'warning',
|
|
1552
|
+
source: 'presentation-collection-coverage',
|
|
1553
|
+
suggestedNextStep:
|
|
1554
|
+
'Enable collection row selection and declare a row identity path so the detail view can bind to selected-row context.',
|
|
1555
|
+
view: viewTrace,
|
|
1556
|
+
}))
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
for (const rowAction of rowActions) {
|
|
1562
|
+
diagnostics.push(
|
|
1563
|
+
...projectCollectionRowActionPredicateCoverageDiagnostics(rowAction, view, viewTrace),
|
|
1564
|
+
)
|
|
1565
|
+
|
|
1566
|
+
const action = findPresentationAction<ActionDefinition>(module, rowAction.ActionId)
|
|
1567
|
+
if (!action) {
|
|
1568
|
+
continue
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const boundParameterNames = new Set(rowAction.Parameters.map((parameter) => parameter.Name))
|
|
1572
|
+
const missingRequiredParameters = action.Parameters.filter((parameter) =>
|
|
1573
|
+
parameter.IsRequired && !boundParameterNames.has(parameter.Name))
|
|
1574
|
+
if (missingRequiredParameters.length > 0) {
|
|
1575
|
+
diagnostics.push(createViewDiagnostic({
|
|
1576
|
+
details: {
|
|
1577
|
+
actionId: action.Id,
|
|
1578
|
+
missingParameters: missingRequiredParameters.map((parameter) => parameter.Name),
|
|
1579
|
+
rowActionId: rowAction.Id,
|
|
1580
|
+
},
|
|
1581
|
+
message:
|
|
1582
|
+
`Collection row action '${rowAction.Id}' invokes '${action.Name}', ` +
|
|
1583
|
+
`but does not bind required parameter(s): ${missingRequiredParameters
|
|
1584
|
+
.map((parameter) => parameter.Name)
|
|
1585
|
+
.join(', ')}.`,
|
|
1586
|
+
reason: `missing-collection-row-action-parameters.${rowAction.Id}`,
|
|
1587
|
+
severity: 'error',
|
|
1588
|
+
source: 'presentation-collection-coverage',
|
|
1589
|
+
view: viewTrace,
|
|
1590
|
+
}))
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
for (const parameter of rowAction.Parameters) {
|
|
1594
|
+
if (parameter.FieldId && !findPresentationField(module, parameter.FieldId)) {
|
|
1595
|
+
diagnostics.push(createViewDiagnostic({
|
|
1596
|
+
details: {
|
|
1597
|
+
actionId: action.Id,
|
|
1598
|
+
fieldId: parameter.FieldId,
|
|
1599
|
+
parameter: parameter.Name,
|
|
1600
|
+
rowActionId: rowAction.Id,
|
|
1601
|
+
},
|
|
1602
|
+
message:
|
|
1603
|
+
`Collection row action '${rowAction.Id}' parameter '${parameter.Name}' ` +
|
|
1604
|
+
`references field '${parameter.FieldId}', but that field is not present in the presentation module.`,
|
|
1605
|
+
reason: `missing-collection-row-action-parameter-field.${rowAction.Id}.${parameter.Name}`,
|
|
1606
|
+
severity: 'error',
|
|
1607
|
+
source: 'presentation-collection-coverage',
|
|
1608
|
+
view: viewTrace,
|
|
1609
|
+
}))
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
for (const selectionAction of selectionActions) {
|
|
1615
|
+
diagnostics.push(
|
|
1616
|
+
...projectCollectionSelectionActionPredicateCoverageDiagnostics(
|
|
1617
|
+
selectionAction,
|
|
1618
|
+
view,
|
|
1619
|
+
viewTrace,
|
|
1620
|
+
),
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
if (!rowIdentityPath) {
|
|
1624
|
+
diagnostics.push(createViewDiagnostic({
|
|
1625
|
+
details: {
|
|
1626
|
+
actionId: selectionAction.ActionId,
|
|
1627
|
+
bodySlotId: bodySlot?.Id ?? null,
|
|
1628
|
+
selectionActionId: selectionAction.Id,
|
|
1629
|
+
},
|
|
1630
|
+
message:
|
|
1631
|
+
`Collection selection action '${selectionAction.Id}' on '${view.Name}' ` +
|
|
1632
|
+
'requires selected row identity context, but the Body chrome slot does not declare a row identity path.',
|
|
1633
|
+
reason: `missing-selection-action-row-identity.${selectionAction.Id}`,
|
|
1634
|
+
severity: 'error',
|
|
1635
|
+
source: 'presentation-collection-coverage',
|
|
1636
|
+
view: viewTrace,
|
|
1637
|
+
}))
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
if (!isCollectionSelectionEnabled(selectionMode)) {
|
|
1641
|
+
diagnostics.push(createViewDiagnostic({
|
|
1642
|
+
details: {
|
|
1643
|
+
actionId: selectionAction.ActionId,
|
|
1644
|
+
selectionActionId: selectionAction.Id,
|
|
1645
|
+
},
|
|
1646
|
+
message:
|
|
1647
|
+
`Collection selection action '${selectionAction.Id}' on '${view.Name}' ` +
|
|
1648
|
+
'is declared, but row selection is not enabled for the collection.',
|
|
1649
|
+
reason: `missing-selection-action-selection-mode.${selectionAction.Id}`,
|
|
1650
|
+
severity: 'warning',
|
|
1651
|
+
source: 'presentation-collection-coverage',
|
|
1652
|
+
view: viewTrace,
|
|
1653
|
+
}))
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
const action = findPresentationAction<ActionDefinition>(module, selectionAction.ActionId)
|
|
1657
|
+
if (!action) {
|
|
1658
|
+
continue
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const boundParameterNames = new Set(selectionAction.Parameters.map((parameter) => parameter.Name))
|
|
1662
|
+
const missingRequiredParameters = action.Parameters.filter((parameter) =>
|
|
1663
|
+
parameter.IsRequired && !boundParameterNames.has(parameter.Name))
|
|
1664
|
+
if (missingRequiredParameters.length > 0) {
|
|
1665
|
+
diagnostics.push(createViewDiagnostic({
|
|
1666
|
+
details: {
|
|
1667
|
+
actionId: action.Id,
|
|
1668
|
+
missingParameters: missingRequiredParameters.map((parameter) => parameter.Name),
|
|
1669
|
+
selectionActionId: selectionAction.Id,
|
|
1670
|
+
},
|
|
1671
|
+
message:
|
|
1672
|
+
`Collection selection action '${selectionAction.Id}' invokes '${action.Name}', ` +
|
|
1673
|
+
`but does not bind required parameter(s): ${missingRequiredParameters
|
|
1674
|
+
.map((parameter) => parameter.Name)
|
|
1675
|
+
.join(', ')}.`,
|
|
1676
|
+
reason: `missing-collection-selection-action-parameters.${selectionAction.Id}`,
|
|
1677
|
+
severity: 'error',
|
|
1678
|
+
source: 'presentation-collection-coverage',
|
|
1679
|
+
view: viewTrace,
|
|
1680
|
+
}))
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
for (const parameter of selectionAction.Parameters) {
|
|
1684
|
+
if (parameter.FieldId && !findPresentationField(module, parameter.FieldId)) {
|
|
1685
|
+
diagnostics.push(createViewDiagnostic({
|
|
1686
|
+
details: {
|
|
1687
|
+
actionId: action.Id,
|
|
1688
|
+
fieldId: parameter.FieldId,
|
|
1689
|
+
parameter: parameter.Name,
|
|
1690
|
+
selectionActionId: selectionAction.Id,
|
|
1691
|
+
},
|
|
1692
|
+
message:
|
|
1693
|
+
`Collection selection action '${selectionAction.Id}' parameter '${parameter.Name}' ` +
|
|
1694
|
+
`references field '${parameter.FieldId}', but that field is not present in the presentation module.`,
|
|
1695
|
+
reason: `missing-collection-selection-action-parameter-field.${selectionAction.Id}.${parameter.Name}`,
|
|
1696
|
+
severity: 'error',
|
|
1697
|
+
source: 'presentation-collection-coverage',
|
|
1698
|
+
view: viewTrace,
|
|
1699
|
+
}))
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
if (isSelectedRowValueSelectionActionParameter(parameter.Source) && !parameter.ValuePath) {
|
|
1703
|
+
diagnostics.push(createViewDiagnostic({
|
|
1704
|
+
details: {
|
|
1705
|
+
actionId: action.Id,
|
|
1706
|
+
parameter: parameter.Name,
|
|
1707
|
+
parameterSource: parameter.Source,
|
|
1708
|
+
selectionActionId: selectionAction.Id,
|
|
1709
|
+
},
|
|
1710
|
+
message:
|
|
1711
|
+
`Collection selection action '${selectionAction.Id}' parameter '${parameter.Name}' ` +
|
|
1712
|
+
'uses selected row values but does not declare a value path.',
|
|
1713
|
+
reason: `missing-collection-selection-action-parameter-value-path.${selectionAction.Id}.${parameter.Name}`,
|
|
1714
|
+
severity: 'error',
|
|
1715
|
+
source: 'presentation-collection-coverage',
|
|
1716
|
+
view: viewTrace,
|
|
1717
|
+
}))
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
return diagnostics
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
function projectCollectionChromeCoverageDiagnostics(
|
|
1726
|
+
module: PresentationModuleDefinition,
|
|
1727
|
+
view: ViewDefinition,
|
|
1728
|
+
viewTrace: PresentationProjectionTraceView,
|
|
1729
|
+
collectionChromeSlotRendererKeys: readonly string[],
|
|
1730
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
1731
|
+
const collection = view.Collection
|
|
1732
|
+
if (!collection) {
|
|
1733
|
+
return []
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
1737
|
+
const slots = resolveProjectedCollectionChromeSlots(collection)
|
|
1738
|
+
diagnostics.push(
|
|
1739
|
+
...projectCollectionChromeCompatibilityDiagnostics(view, viewTrace, collection, slots),
|
|
1740
|
+
...projectCollectionChromeRequiredSlotDiagnostics(module, view, viewTrace, collection),
|
|
1741
|
+
)
|
|
1742
|
+
const seenSlotIds = new Set<string>()
|
|
1743
|
+
|
|
1744
|
+
for (const slot of slots) {
|
|
1745
|
+
if (seenSlotIds.has(slot.Id)) {
|
|
1746
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
1747
|
+
details: { slotId: slot.Id },
|
|
1748
|
+
message:
|
|
1749
|
+
`Collection view '${view.Name}' declares collection chrome slot ` +
|
|
1750
|
+
`'${slot.Id}' more than once.`,
|
|
1751
|
+
reason: `duplicate-collection-chrome-slot.${slot.Id}`,
|
|
1752
|
+
severity: 'warning',
|
|
1753
|
+
slot,
|
|
1754
|
+
view: viewTrace,
|
|
1755
|
+
}))
|
|
1756
|
+
}
|
|
1757
|
+
seenSlotIds.add(slot.Id)
|
|
1758
|
+
|
|
1759
|
+
diagnostics.push(
|
|
1760
|
+
...projectCollectionChromeSlotReferenceDiagnostics(module, view, viewTrace, slot),
|
|
1761
|
+
...projectCollectionChromeSlotInterpretationDiagnostics(
|
|
1762
|
+
view,
|
|
1763
|
+
viewTrace,
|
|
1764
|
+
slot,
|
|
1765
|
+
collectionChromeSlotRendererKeys,
|
|
1766
|
+
),
|
|
1767
|
+
)
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
return diagnostics
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function projectCollectionOuterViewDuplicationDiagnostics(
|
|
1774
|
+
view: ViewDefinition,
|
|
1775
|
+
viewTrace: PresentationProjectionTraceView,
|
|
1776
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
1777
|
+
if (!view.Collection) {
|
|
1778
|
+
return []
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const duplicatedFields = [
|
|
1782
|
+
view.Subject.DataSourceId ? 'Subject.DataSourceId' : null,
|
|
1783
|
+
view.DataSourceIds.length > 0 ? 'DataSourceIds' : null,
|
|
1784
|
+
view.FieldIds.length > 0 ? 'FieldIds' : null,
|
|
1785
|
+
view.Actions.length > 0 ? 'Actions' : null,
|
|
1786
|
+
].filter((field): field is string => Boolean(field))
|
|
1787
|
+
|
|
1788
|
+
if (duplicatedFields.length === 0) {
|
|
1789
|
+
return []
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return [
|
|
1793
|
+
createViewDiagnostic({
|
|
1794
|
+
category: 'incomplete-projection',
|
|
1795
|
+
details: { duplicatedFields },
|
|
1796
|
+
interpretation: {
|
|
1797
|
+
status: 'unsupported',
|
|
1798
|
+
target: 'collection-chrome-single-source-of-truth',
|
|
1799
|
+
},
|
|
1800
|
+
message:
|
|
1801
|
+
`Collection view '${view.Name}' still declares collection-specific ` +
|
|
1802
|
+
`outer view data: ${duplicatedFields.join(', ')}.`,
|
|
1803
|
+
reason: `duplicated-collection-outer-view-data.${view.Id}`,
|
|
1804
|
+
severity: 'warning',
|
|
1805
|
+
source: 'presentation-collection-coverage',
|
|
1806
|
+
suggestedNextStep:
|
|
1807
|
+
'Move collection data sources, fields, actions, and query/detail semantics into Collection.Chrome slots.',
|
|
1808
|
+
view: viewTrace,
|
|
1809
|
+
}),
|
|
1810
|
+
]
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function projectProjectedCollectionActionContextDiagnostics({
|
|
1814
|
+
bodySlot,
|
|
1815
|
+
collectionChromeSlotRendererKeys,
|
|
1816
|
+
projectedActions,
|
|
1817
|
+
selectionMode,
|
|
1818
|
+
selectionStateId,
|
|
1819
|
+
view,
|
|
1820
|
+
viewTrace,
|
|
1821
|
+
}: {
|
|
1822
|
+
readonly bodySlot: CollectionChromeSlotDefinition | null
|
|
1823
|
+
readonly collectionChromeSlotRendererKeys: readonly string[]
|
|
1824
|
+
readonly projectedActions: readonly PresentationViewProjectedActionRef[]
|
|
1825
|
+
readonly selectionMode: unknown
|
|
1826
|
+
readonly selectionStateId: string | null
|
|
1827
|
+
readonly view: ViewDefinition
|
|
1828
|
+
readonly viewTrace: PresentationProjectionTraceView
|
|
1829
|
+
}): readonly PresentationProjectionDiagnostic[] {
|
|
1830
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
1831
|
+
const bodySlotRendererBound = bodySlot
|
|
1832
|
+
? hasCollectionChromeSlotRendererBinding(collectionChromeSlotRendererKeys, bodySlot)
|
|
1833
|
+
: false
|
|
1834
|
+
|
|
1835
|
+
for (const actionRef of projectedActions) {
|
|
1836
|
+
if (actionRef.contextKind === 'collection-row') {
|
|
1837
|
+
if (!bodySlot) {
|
|
1838
|
+
diagnostics.push(createViewDiagnostic({
|
|
1839
|
+
category: 'missing-binding',
|
|
1840
|
+
details: {
|
|
1841
|
+
actionId: actionRef.actionId,
|
|
1842
|
+
actionSource: actionRef.source,
|
|
1843
|
+
collectionActionContextKind: actionRef.contextKind,
|
|
1844
|
+
rowActionId: actionRef.rowAction?.Id ?? null,
|
|
1845
|
+
slotId: actionRef.slotId ?? null,
|
|
1846
|
+
},
|
|
1847
|
+
interpretation: {
|
|
1848
|
+
status: 'unbound',
|
|
1849
|
+
target: 'collection-row-action-context',
|
|
1850
|
+
},
|
|
1851
|
+
message:
|
|
1852
|
+
`Collection row action '${actionRef.actionId}' on '${view.Name}' ` +
|
|
1853
|
+
'requires row context, but the collection does not declare a Body chrome slot.',
|
|
1854
|
+
reason: `missing-collection-row-action-context.${actionRef.source}`,
|
|
1855
|
+
severity: 'error',
|
|
1856
|
+
source: 'presentation-action-context-coverage',
|
|
1857
|
+
suggestedNextStep:
|
|
1858
|
+
'Declare a Body chrome slot so row actions can be interpreted against collection rows.',
|
|
1859
|
+
view: viewTrace,
|
|
1860
|
+
}))
|
|
1861
|
+
} else if (!bodySlotRendererBound) {
|
|
1862
|
+
diagnostics.push(createViewDiagnostic({
|
|
1863
|
+
category: 'missing-binding',
|
|
1864
|
+
details: {
|
|
1865
|
+
actionId: actionRef.actionId,
|
|
1866
|
+
actionSource: actionRef.source,
|
|
1867
|
+
bodySlotId: bodySlot.Id,
|
|
1868
|
+
collectionActionContextKind: actionRef.contextKind,
|
|
1869
|
+
rowActionId: actionRef.rowAction?.Id ?? null,
|
|
1870
|
+
slotId: actionRef.slotId ?? null,
|
|
1871
|
+
},
|
|
1872
|
+
interpretation: {
|
|
1873
|
+
status: 'unbound',
|
|
1874
|
+
target: 'collection-row-action-context-renderer',
|
|
1875
|
+
},
|
|
1876
|
+
message:
|
|
1877
|
+
`Collection row action '${actionRef.actionId}' on '${view.Name}' ` +
|
|
1878
|
+
`requires row context from Body chrome slot '${bodySlot.Id}', but no Body slot renderer is bound.`,
|
|
1879
|
+
reason: `unbound-collection-row-action-context-renderer.${actionRef.source}`,
|
|
1880
|
+
severity: 'warning',
|
|
1881
|
+
source: 'presentation-action-context-coverage',
|
|
1882
|
+
suggestedNextStep:
|
|
1883
|
+
'Bind a Body chrome slot renderer that supplies row context to collection row actions.',
|
|
1884
|
+
view: viewTrace,
|
|
1885
|
+
}))
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
const missingRowValueParameters = actionRef.rowAction?.Parameters
|
|
1889
|
+
.filter((parameter) => !parameter.ValuePath)
|
|
1890
|
+
.map((parameter) => parameter.Name) ?? []
|
|
1891
|
+
if (missingRowValueParameters.length > 0) {
|
|
1892
|
+
diagnostics.push(createViewDiagnostic({
|
|
1893
|
+
details: {
|
|
1894
|
+
actionId: actionRef.actionId,
|
|
1895
|
+
actionSource: actionRef.source,
|
|
1896
|
+
missingRowValueParameters,
|
|
1897
|
+
rowActionId: actionRef.rowAction?.Id ?? null,
|
|
1898
|
+
},
|
|
1899
|
+
message:
|
|
1900
|
+
`Collection row action '${actionRef.rowAction?.Id ?? actionRef.actionId}' ` +
|
|
1901
|
+
`on '${view.Name}' has parameter(s) without row value paths: ${missingRowValueParameters.join(', ')}.`,
|
|
1902
|
+
reason: `missing-collection-row-action-value-path.${actionRef.source}`,
|
|
1903
|
+
severity: 'error',
|
|
1904
|
+
source: 'presentation-action-context-coverage',
|
|
1905
|
+
view: viewTrace,
|
|
1906
|
+
}))
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
if (actionRef.contextKind === 'collection-selection') {
|
|
1911
|
+
if (!isCollectionSelectionEnabled(selectionMode)) {
|
|
1912
|
+
diagnostics.push(createViewDiagnostic({
|
|
1913
|
+
category: 'missing-binding',
|
|
1914
|
+
details: {
|
|
1915
|
+
actionId: actionRef.actionId,
|
|
1916
|
+
actionSource: actionRef.source,
|
|
1917
|
+
collectionActionContextKind: actionRef.contextKind,
|
|
1918
|
+
selectionActionId: actionRef.selectionAction?.Id ?? null,
|
|
1919
|
+
selectionMode,
|
|
1920
|
+
slotId: actionRef.slotId ?? null,
|
|
1921
|
+
},
|
|
1922
|
+
interpretation: {
|
|
1923
|
+
status: 'unbound',
|
|
1924
|
+
target: 'collection-selection-action-context',
|
|
1925
|
+
},
|
|
1926
|
+
message:
|
|
1927
|
+
`Collection selection action '${actionRef.actionId}' on '${view.Name}' ` +
|
|
1928
|
+
'requires selection context, but row selection is not enabled.',
|
|
1929
|
+
reason: `missing-collection-selection-action-context.${actionRef.source}`,
|
|
1930
|
+
severity: 'warning',
|
|
1931
|
+
source: 'presentation-action-context-coverage',
|
|
1932
|
+
suggestedNextStep:
|
|
1933
|
+
'Enable collection row selection, or model the action as a plain view/slot action.',
|
|
1934
|
+
view: viewTrace,
|
|
1935
|
+
}))
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
if (!selectionStateId) {
|
|
1939
|
+
diagnostics.push(createViewDiagnostic({
|
|
1940
|
+
category: 'missing-binding',
|
|
1941
|
+
details: {
|
|
1942
|
+
actionId: actionRef.actionId,
|
|
1943
|
+
actionSource: actionRef.source,
|
|
1944
|
+
collectionActionContextKind: actionRef.contextKind,
|
|
1945
|
+
selectionActionId: actionRef.selectionAction?.Id ?? null,
|
|
1946
|
+
slotId: actionRef.slotId ?? null,
|
|
1947
|
+
},
|
|
1948
|
+
interpretation: {
|
|
1949
|
+
status: 'unbound',
|
|
1950
|
+
target: 'collection-selection-action-state',
|
|
1951
|
+
},
|
|
1952
|
+
message:
|
|
1953
|
+
`Collection selection action '${actionRef.actionId}' on '${view.Name}' ` +
|
|
1954
|
+
'requires selected-row state, but no selection StateId is declared.',
|
|
1955
|
+
reason: `missing-collection-selection-action-state.${actionRef.source}`,
|
|
1956
|
+
severity: 'warning',
|
|
1957
|
+
source: 'presentation-action-context-coverage',
|
|
1958
|
+
suggestedNextStep:
|
|
1959
|
+
'Declare StateId on the collection Body or SelectionActions chrome slot.',
|
|
1960
|
+
view: viewTrace,
|
|
1961
|
+
}))
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (!actionRef.context.selectedRowIdentityPath) {
|
|
1965
|
+
diagnostics.push(createViewDiagnostic({
|
|
1966
|
+
details: {
|
|
1967
|
+
actionId: actionRef.actionId,
|
|
1968
|
+
actionSource: actionRef.source,
|
|
1969
|
+
collectionActionContextKind: actionRef.contextKind,
|
|
1970
|
+
selectionActionId: actionRef.selectionAction?.Id ?? null,
|
|
1971
|
+
slotId: actionRef.slotId ?? null,
|
|
1972
|
+
},
|
|
1973
|
+
message:
|
|
1974
|
+
`Collection selection action '${actionRef.actionId}' on '${view.Name}' ` +
|
|
1975
|
+
'requires selected-row context, but the collection Body slot does not declare RowIdentityPath.',
|
|
1976
|
+
reason: `missing-collection-selection-action-row-identity.${actionRef.source}`,
|
|
1977
|
+
severity: 'error',
|
|
1978
|
+
source: 'presentation-action-context-coverage',
|
|
1979
|
+
view: viewTrace,
|
|
1980
|
+
}))
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
return diagnostics
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function resolveCollectionChromeBodySlot(
|
|
1989
|
+
collection: NonNullable<ViewDefinition['Collection']>,
|
|
1990
|
+
) {
|
|
1991
|
+
return resolveProjectedCollectionChromeSlots(collection)
|
|
1992
|
+
.find((slot) => isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.body)) ?? null
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
function resolveCollectionChromeDetailSlot(
|
|
1996
|
+
collection: NonNullable<ViewDefinition['Collection']>,
|
|
1997
|
+
) {
|
|
1998
|
+
return resolveProjectedCollectionChromeSlots(collection)
|
|
1999
|
+
.find((slot) => isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.detail)) ?? null
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function resolveCollectionChromeRowActions(
|
|
2003
|
+
collection: NonNullable<ViewDefinition['Collection']>,
|
|
2004
|
+
) {
|
|
2005
|
+
return sortCollectionActionBindings(
|
|
2006
|
+
uniqueCollectionActionBindings(
|
|
2007
|
+
resolveProjectedCollectionChromeSlots(collection)
|
|
2008
|
+
.filter((slot) => isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.rowActions))
|
|
2009
|
+
.flatMap((slot) => slot.RowActions),
|
|
2010
|
+
),
|
|
2011
|
+
)
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function resolveCollectionChromeSelectionActions(
|
|
2015
|
+
collection: NonNullable<ViewDefinition['Collection']>,
|
|
2016
|
+
) {
|
|
2017
|
+
return sortCollectionActionBindings(
|
|
2018
|
+
uniqueCollectionActionBindings(
|
|
2019
|
+
resolveProjectedCollectionChromeSlots(collection)
|
|
2020
|
+
.filter((slot) =>
|
|
2021
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.selectionActions))
|
|
2022
|
+
.flatMap((slot) => slot.SelectionActions),
|
|
2023
|
+
),
|
|
2024
|
+
)
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
function uniqueCollectionActionBindings<TAction extends { readonly Id: string }>(
|
|
2028
|
+
actions: readonly TAction[],
|
|
2029
|
+
) {
|
|
2030
|
+
const seen = new Set<string>()
|
|
2031
|
+
return actions.filter((action) => {
|
|
2032
|
+
if (seen.has(action.Id)) {
|
|
2033
|
+
return false
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
seen.add(action.Id)
|
|
2037
|
+
return true
|
|
2038
|
+
})
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
function sortCollectionActionBindings<TAction extends { readonly Order: number }>(
|
|
2042
|
+
actions: readonly TAction[],
|
|
2043
|
+
) {
|
|
2044
|
+
return actions.slice().sort((left, right) => left.Order - right.Order)
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function projectCollectionChromeCompatibilityDiagnostics(
|
|
2048
|
+
view: ViewDefinition,
|
|
2049
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2050
|
+
collection: NonNullable<ViewDefinition['Collection']>,
|
|
2051
|
+
slots: readonly CollectionChromeSlotDefinition[],
|
|
2052
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2053
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
2054
|
+
const declaredSlots = collection.Chrome?.Slots ?? []
|
|
2055
|
+
const projectionMode = readCollectionChromeProjectionMode(collection.Chrome)
|
|
2056
|
+
const synthesizedSlots = slots.filter((slot) =>
|
|
2057
|
+
isCollectionChromeSlotProjectionMode(
|
|
2058
|
+
slot,
|
|
2059
|
+
collectionChromeProjectionModes.compatibilitySynthesized,
|
|
2060
|
+
))
|
|
2061
|
+
|
|
2062
|
+
if (
|
|
2063
|
+
projectionMode === collectionChromeProjectionModes.compatibilitySynthesized ||
|
|
2064
|
+
synthesizedSlots.length > 0
|
|
2065
|
+
) {
|
|
2066
|
+
diagnostics.push(createViewDiagnostic({
|
|
2067
|
+
category: 'incomplete-projection',
|
|
2068
|
+
details: {
|
|
2069
|
+
declaredSlotIds: declaredSlots.map((slot) => slot.Id),
|
|
2070
|
+
projectionMode:
|
|
2071
|
+
projectionMode ?? collectionChromeProjectionModes.compatibilitySynthesized,
|
|
2072
|
+
synthesizedSlotIds: synthesizedSlots.map((slot) => slot.Id),
|
|
2073
|
+
},
|
|
2074
|
+
interpretation: {
|
|
2075
|
+
status: 'unsupported',
|
|
2076
|
+
target: 'projected-collection-runtime.chrome',
|
|
2077
|
+
},
|
|
2078
|
+
message:
|
|
2079
|
+
`Collection view '${view.Name}' uses compatibility-synthesized collection chrome; ` +
|
|
2080
|
+
'the runtime no longer synthesizes chrome from legacy collection fields.',
|
|
2081
|
+
reason: 'synthesized-collection-chrome',
|
|
2082
|
+
severity: 'warning',
|
|
2083
|
+
source: 'presentation-collection-chrome-coverage',
|
|
2084
|
+
suggestedNextStep:
|
|
2085
|
+
'Declare Collection.Chrome slots on the backend, including Body, QueryForm, Summary, RowActions, Detail, SelectionActions, and Pagination as needed.',
|
|
2086
|
+
view: viewTrace,
|
|
2087
|
+
}))
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
if (declaredSlots.length === 0) {
|
|
2091
|
+
diagnostics.push(createViewDiagnostic({
|
|
2092
|
+
category: 'incomplete-projection',
|
|
2093
|
+
details: {
|
|
2094
|
+
declaredSlotIds: declaredSlots.map((slot) => slot.Id),
|
|
2095
|
+
projectionMode: projectionMode ?? null,
|
|
2096
|
+
},
|
|
2097
|
+
interpretation: {
|
|
2098
|
+
status: 'unbound',
|
|
2099
|
+
target: 'projected-collection-runtime.chrome',
|
|
2100
|
+
},
|
|
2101
|
+
message:
|
|
2102
|
+
`Collection view '${view.Name}' does not declare Collection.Chrome slots; ` +
|
|
2103
|
+
'collection rendering now requires declared chrome, including a Body slot.',
|
|
2104
|
+
reason: 'missing-collection-chrome',
|
|
2105
|
+
severity: 'error',
|
|
2106
|
+
source: 'presentation-collection-chrome-coverage',
|
|
2107
|
+
suggestedNextStep:
|
|
2108
|
+
'Declare Collection.Chrome slots on the backend, including Body, QueryForm, Summary, RowActions, Detail, SelectionActions, and Pagination as needed.',
|
|
2109
|
+
view: viewTrace,
|
|
2110
|
+
}))
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
if (projectionMode === collectionChromeProjectionModes.mixed) {
|
|
2114
|
+
diagnostics.push(createViewDiagnostic({
|
|
2115
|
+
category: 'incomplete-projection',
|
|
2116
|
+
details: {
|
|
2117
|
+
declaredSlotIds: declaredSlots.map((slot) => slot.Id),
|
|
2118
|
+
projectionMode,
|
|
2119
|
+
},
|
|
2120
|
+
interpretation: {
|
|
2121
|
+
status: 'locally-interpreted',
|
|
2122
|
+
target: 'projected-collection-runtime.chrome.mixed',
|
|
2123
|
+
},
|
|
2124
|
+
message:
|
|
2125
|
+
`Collection view '${view.Name}' declares mixed collection chrome projection; ` +
|
|
2126
|
+
'some collection surface behavior may still rely on frontend compatibility interpretation.',
|
|
2127
|
+
reason: 'mixed-collection-chrome-projection',
|
|
2128
|
+
severity: 'info',
|
|
2129
|
+
source: 'presentation-collection-chrome-coverage',
|
|
2130
|
+
suggestedNextStep:
|
|
2131
|
+
'Replace mixed collection chrome with fully declared slots once the remaining compatibility behavior is modeled.',
|
|
2132
|
+
view: viewTrace,
|
|
2133
|
+
}))
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
if (
|
|
2137
|
+
declaredSlots.length > 0 &&
|
|
2138
|
+
projectionMode !== collectionChromeProjectionModes.compatibilitySynthesized &&
|
|
2139
|
+
!declaredSlots.some((slot) =>
|
|
2140
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.body))
|
|
2141
|
+
) {
|
|
2142
|
+
const isMixedProjection = projectionMode === collectionChromeProjectionModes.mixed
|
|
2143
|
+
diagnostics.push(createViewDiagnostic({
|
|
2144
|
+
category: 'incomplete-projection',
|
|
2145
|
+
details: {
|
|
2146
|
+
declaredSlotIds: declaredSlots.map((slot) => slot.Id),
|
|
2147
|
+
projectionMode: projectionMode ?? null,
|
|
2148
|
+
},
|
|
2149
|
+
interpretation: {
|
|
2150
|
+
status: 'unbound',
|
|
2151
|
+
target: 'projected-collection-view.body-slot',
|
|
2152
|
+
},
|
|
2153
|
+
message:
|
|
2154
|
+
`Collection view '${view.Name}' declares collection chrome without a Body slot; ` +
|
|
2155
|
+
'the collection body will not render until Body is declared.',
|
|
2156
|
+
reason: 'missing-collection-body-slot',
|
|
2157
|
+
severity: isMixedProjection ? 'warning' : 'error',
|
|
2158
|
+
source: 'presentation-collection-chrome-coverage',
|
|
2159
|
+
suggestedNextStep:
|
|
2160
|
+
'Add a Collection.Chrome Body slot so the collection body is projected through the chrome slot interpreter.',
|
|
2161
|
+
view: viewTrace,
|
|
2162
|
+
}))
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
return diagnostics
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function projectCollectionChromeRequiredSlotDiagnostics(
|
|
2169
|
+
module: PresentationModuleDefinition,
|
|
2170
|
+
view: ViewDefinition,
|
|
2171
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2172
|
+
collection: NonNullable<ViewDefinition['Collection']>,
|
|
2173
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2174
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
2175
|
+
const chrome = createCollectionChromeRuntime(collection)
|
|
2176
|
+
|
|
2177
|
+
if (chrome.bodySlot && chrome.bodySlot.DataSourceIds.length === 0) {
|
|
2178
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2179
|
+
details: { slotId: chrome.bodySlot.Id },
|
|
2180
|
+
interpretation: {
|
|
2181
|
+
status: 'unbound',
|
|
2182
|
+
target: 'projected-collection-runtime.body-data-source',
|
|
2183
|
+
},
|
|
2184
|
+
message:
|
|
2185
|
+
`Collection view '${view.Name}' declares Body chrome slot ` +
|
|
2186
|
+
`'${chrome.bodySlot.Id}', but the slot does not declare a data source.`,
|
|
2187
|
+
reason: `missing-collection-body-slot-data-source.${chrome.bodySlot.Id}`,
|
|
2188
|
+
severity: 'error',
|
|
2189
|
+
slot: chrome.bodySlot,
|
|
2190
|
+
suggestedNextStep:
|
|
2191
|
+
'Declare the collection row data source id on the Body chrome slot.',
|
|
2192
|
+
view: viewTrace,
|
|
2193
|
+
}))
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
for (const queryFormSlot of chrome.findSlots(collectionChromeSlotKinds.queryForm)) {
|
|
2197
|
+
if (!queryFormSlot.QueryFormId) {
|
|
2198
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2199
|
+
details: { slotId: queryFormSlot.Id },
|
|
2200
|
+
interpretation: {
|
|
2201
|
+
status: 'unbound',
|
|
2202
|
+
target: 'projected-collection-runtime.query-form-slot',
|
|
2203
|
+
},
|
|
2204
|
+
message:
|
|
2205
|
+
`Collection view '${view.Name}' declares QueryForm chrome slot ` +
|
|
2206
|
+
`'${queryFormSlot.Id}', but the slot does not declare QueryFormId.`,
|
|
2207
|
+
reason: `missing-collection-query-form-slot-query-form.${queryFormSlot.Id}`,
|
|
2208
|
+
severity: 'error',
|
|
2209
|
+
slot: queryFormSlot,
|
|
2210
|
+
suggestedNextStep:
|
|
2211
|
+
'Declare QueryFormId on the QueryForm chrome slot.',
|
|
2212
|
+
view: viewTrace,
|
|
2213
|
+
}))
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
for (const paginationSlot of chrome.findSlots(collectionChromeSlotKinds.pagination)) {
|
|
2218
|
+
if (paginationSlot.DataSourceIds.length === 0) {
|
|
2219
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2220
|
+
details: { slotId: paginationSlot.Id },
|
|
2221
|
+
interpretation: {
|
|
2222
|
+
status: 'unbound',
|
|
2223
|
+
target: 'projected-collection-runtime.pagination-data-source',
|
|
2224
|
+
},
|
|
2225
|
+
message:
|
|
2226
|
+
`Collection view '${view.Name}' declares Pagination chrome slot ` +
|
|
2227
|
+
`'${paginationSlot.Id}', but the slot does not declare a paginated data source.`,
|
|
2228
|
+
reason: `missing-collection-pagination-slot-data-source.${paginationSlot.Id}`,
|
|
2229
|
+
severity: 'error',
|
|
2230
|
+
slot: paginationSlot,
|
|
2231
|
+
suggestedNextStep:
|
|
2232
|
+
'Declare the paginated collection-query data source id on the Pagination chrome slot.',
|
|
2233
|
+
view: viewTrace,
|
|
2234
|
+
}))
|
|
2235
|
+
continue
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
for (const dataSourceId of paginationSlot.DataSourceIds) {
|
|
2239
|
+
const dataSource = findPresentationDataSource(module, dataSourceId)
|
|
2240
|
+
if (!dataSource) {
|
|
2241
|
+
continue
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (!isCollectionQueryDataSource(dataSource)) {
|
|
2245
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2246
|
+
details: {
|
|
2247
|
+
dataSourceId,
|
|
2248
|
+
dataSourceKind: dataSource.Kind,
|
|
2249
|
+
slotId: paginationSlot.Id,
|
|
2250
|
+
},
|
|
2251
|
+
interpretation: {
|
|
2252
|
+
status: 'unsupported',
|
|
2253
|
+
target: 'projected-collection-runtime.pagination-data-source',
|
|
2254
|
+
},
|
|
2255
|
+
message:
|
|
2256
|
+
`Collection view '${view.Name}' declares Pagination chrome slot ` +
|
|
2257
|
+
`'${paginationSlot.Id}' for data source '${dataSourceId}', but that data source is not a collection query.`,
|
|
2258
|
+
reason: `unsupported-collection-pagination-data-source-kind.${paginationSlot.Id}.${dataSourceId}`,
|
|
2259
|
+
severity: 'error',
|
|
2260
|
+
slot: paginationSlot,
|
|
2261
|
+
suggestedNextStep:
|
|
2262
|
+
'Point the Pagination chrome slot at a collection-query data source with pagination metadata.',
|
|
2263
|
+
view: viewTrace,
|
|
2264
|
+
}))
|
|
2265
|
+
continue
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
if (!dataSource.Query?.Pagination) {
|
|
2269
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2270
|
+
details: { dataSourceId, slotId: paginationSlot.Id },
|
|
2271
|
+
interpretation: {
|
|
2272
|
+
status: 'unbound',
|
|
2273
|
+
target: 'projected-collection-runtime.pagination-window',
|
|
2274
|
+
},
|
|
2275
|
+
message:
|
|
2276
|
+
`Collection view '${view.Name}' declares Pagination chrome slot ` +
|
|
2277
|
+
`'${paginationSlot.Id}' for '${dataSourceId}', but the data source does not declare query pagination.`,
|
|
2278
|
+
reason: `missing-collection-pagination-data-source-policy.${paginationSlot.Id}.${dataSourceId}`,
|
|
2279
|
+
severity: 'error',
|
|
2280
|
+
slot: paginationSlot,
|
|
2281
|
+
suggestedNextStep:
|
|
2282
|
+
'Declare DataSource.Query.Pagination so the collection pagination window can be interpreted.',
|
|
2283
|
+
view: viewTrace,
|
|
2284
|
+
}))
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
for (const detailSlot of chrome.findSlots(collectionChromeSlotKinds.detail)) {
|
|
2290
|
+
if (!detailSlot.DetailViewId) {
|
|
2291
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2292
|
+
details: { slotId: detailSlot.Id },
|
|
2293
|
+
interpretation: {
|
|
2294
|
+
status: 'unbound',
|
|
2295
|
+
target: 'projected-collection-runtime.detail-slot',
|
|
2296
|
+
},
|
|
2297
|
+
message:
|
|
2298
|
+
`Collection view '${view.Name}' declares Detail chrome slot ` +
|
|
2299
|
+
`'${detailSlot.Id}', but the slot does not declare DetailViewId.`,
|
|
2300
|
+
reason: `missing-collection-detail-slot-view.${detailSlot.Id}`,
|
|
2301
|
+
severity: 'error',
|
|
2302
|
+
slot: detailSlot,
|
|
2303
|
+
suggestedNextStep:
|
|
2304
|
+
'Declare DetailViewId on the Detail chrome slot.',
|
|
2305
|
+
view: viewTrace,
|
|
2306
|
+
}))
|
|
2307
|
+
continue
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
if (
|
|
2311
|
+
detailSlot.DetailActivation == null ||
|
|
2312
|
+
isCollectionDetailActivationNone(detailSlot.DetailActivation)
|
|
2313
|
+
) {
|
|
2314
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2315
|
+
details: {
|
|
2316
|
+
detailActivation: detailSlot.DetailActivation ?? null,
|
|
2317
|
+
detailViewId: detailSlot.DetailViewId,
|
|
2318
|
+
slotId: detailSlot.Id,
|
|
2319
|
+
},
|
|
2320
|
+
interpretation: {
|
|
2321
|
+
status: 'unbound',
|
|
2322
|
+
target: 'projected-collection-runtime.detail-activation',
|
|
2323
|
+
},
|
|
2324
|
+
message:
|
|
2325
|
+
`Collection view '${view.Name}' declares Detail chrome slot ` +
|
|
2326
|
+
`'${detailSlot.Id}', but the slot does not declare a detail activation mode.`,
|
|
2327
|
+
reason: `missing-collection-detail-slot-activation.${detailSlot.Id}`,
|
|
2328
|
+
severity: 'error',
|
|
2329
|
+
slot: detailSlot,
|
|
2330
|
+
suggestedNextStep:
|
|
2331
|
+
'Declare DetailActivation on the Detail chrome slot so the frontend can supply row context.',
|
|
2332
|
+
view: viewTrace,
|
|
2333
|
+
}))
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
if (
|
|
2337
|
+
isCollectionDetailActivationStateful(detailSlot.DetailActivation) &&
|
|
2338
|
+
!detailSlot.StateId &&
|
|
2339
|
+
!chrome.bodySlot?.StateId
|
|
2340
|
+
) {
|
|
2341
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2342
|
+
details: {
|
|
2343
|
+
detailActivation: detailSlot.DetailActivation,
|
|
2344
|
+
detailViewId: detailSlot.DetailViewId,
|
|
2345
|
+
slotId: detailSlot.Id,
|
|
2346
|
+
},
|
|
2347
|
+
interpretation: {
|
|
2348
|
+
status: 'unbound',
|
|
2349
|
+
target: 'projected-collection-runtime.detail-state',
|
|
2350
|
+
},
|
|
2351
|
+
message:
|
|
2352
|
+
`Collection view '${view.Name}' declares stateful Detail chrome slot ` +
|
|
2353
|
+
`'${detailSlot.Id}', but neither the Detail nor Body slot declares StateId.`,
|
|
2354
|
+
reason: `missing-collection-detail-slot-state.${detailSlot.Id}`,
|
|
2355
|
+
severity: 'warning',
|
|
2356
|
+
slot: detailSlot,
|
|
2357
|
+
suggestedNextStep:
|
|
2358
|
+
'Declare StateId on the Body or Detail chrome slot so detail context can be coordinated with selection state.',
|
|
2359
|
+
view: viewTrace,
|
|
2360
|
+
}))
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
return diagnostics
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function projectCollectionChromeSlotReferenceDiagnostics(
|
|
2368
|
+
module: PresentationModuleDefinition,
|
|
2369
|
+
view: ViewDefinition,
|
|
2370
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2371
|
+
slot: CollectionChromeSlotDefinition,
|
|
2372
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2373
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
2374
|
+
|
|
2375
|
+
for (const dataSourceId of slot.DataSourceIds) {
|
|
2376
|
+
if (!findPresentationDataSource(module, dataSourceId)) {
|
|
2377
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2378
|
+
details: { dataSourceId, slotId: slot.Id },
|
|
2379
|
+
message:
|
|
2380
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' references ` +
|
|
2381
|
+
`data source '${dataSourceId}', but that data source is not present in the presentation module.`,
|
|
2382
|
+
reason: `missing-collection-chrome-slot-data-source.${slot.Id}.${dataSourceId}`,
|
|
2383
|
+
severity: 'error',
|
|
2384
|
+
slot,
|
|
2385
|
+
view: viewTrace,
|
|
2386
|
+
}))
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
if (slot.QueryFormId && !findPresentationQueryForm(module, slot.QueryFormId)) {
|
|
2391
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2392
|
+
details: { queryFormId: slot.QueryFormId, slotId: slot.Id },
|
|
2393
|
+
message:
|
|
2394
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' references ` +
|
|
2395
|
+
`query form '${slot.QueryFormId}', but that query form is not present in the presentation module.`,
|
|
2396
|
+
reason: `missing-collection-chrome-slot-query-form.${slot.Id}.${slot.QueryFormId}`,
|
|
2397
|
+
severity: 'error',
|
|
2398
|
+
slot,
|
|
2399
|
+
view: viewTrace,
|
|
2400
|
+
}))
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
if (slot.DetailViewId && !findPresentationView(module, slot.DetailViewId)) {
|
|
2404
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2405
|
+
details: { detailViewId: slot.DetailViewId, slotId: slot.Id },
|
|
2406
|
+
message:
|
|
2407
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' references ` +
|
|
2408
|
+
`detail view '${slot.DetailViewId}', but that view is not present in the presentation module.`,
|
|
2409
|
+
reason: `missing-collection-chrome-slot-detail-view.${slot.Id}.${slot.DetailViewId}`,
|
|
2410
|
+
severity: 'error',
|
|
2411
|
+
slot,
|
|
2412
|
+
view: viewTrace,
|
|
2413
|
+
}))
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
for (const fieldId of slot.FieldIds) {
|
|
2417
|
+
if (!findPresentationField(module, fieldId)) {
|
|
2418
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2419
|
+
details: { fieldId, slotId: slot.Id },
|
|
2420
|
+
message:
|
|
2421
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' references ` +
|
|
2422
|
+
`field '${fieldId}', but that field is not present in the presentation module.`,
|
|
2423
|
+
reason: `missing-collection-chrome-slot-field.${slot.Id}.${fieldId}`,
|
|
2424
|
+
severity: 'error',
|
|
2425
|
+
slot,
|
|
2426
|
+
view: viewTrace,
|
|
2427
|
+
}))
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
for (const column of slot.Columns) {
|
|
2432
|
+
if (!findPresentationField(module, column.FieldId)) {
|
|
2433
|
+
diagnostics.push(createCollectionChromeSlotDiagnostic({
|
|
2434
|
+
details: {
|
|
2435
|
+
columnId: column.Id,
|
|
2436
|
+
fieldId: column.FieldId,
|
|
2437
|
+
slotId: slot.Id,
|
|
2438
|
+
},
|
|
2439
|
+
message:
|
|
2440
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' declares ` +
|
|
2441
|
+
`column '${column.Id}' with field '${column.FieldId}', but that field is not present in the presentation module.`,
|
|
2442
|
+
reason: `missing-collection-chrome-slot-column-field.${slot.Id}.${column.Id}.${column.FieldId}`,
|
|
2443
|
+
severity: 'error',
|
|
2444
|
+
slot,
|
|
2445
|
+
view: viewTrace,
|
|
2446
|
+
}))
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
return diagnostics
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function projectCollectionChromeSlotInterpretationDiagnostics(
|
|
2454
|
+
view: ViewDefinition,
|
|
2455
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2456
|
+
slot: CollectionChromeSlotDefinition,
|
|
2457
|
+
collectionChromeSlotRendererKeys: readonly string[],
|
|
2458
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2459
|
+
const candidateRendererKeys = getCollectionChromeSlotRendererCandidateKeys(slot)
|
|
2460
|
+
if (hasCollectionChromeSlotRendererBinding(collectionChromeSlotRendererKeys, slot)) {
|
|
2461
|
+
return [
|
|
2462
|
+
createCollectionChromeSlotDiagnostic({
|
|
2463
|
+
category: 'local-interpretation',
|
|
2464
|
+
details: {
|
|
2465
|
+
candidateRendererKeys,
|
|
2466
|
+
slotId: slot.Id,
|
|
2467
|
+
slotKind: slot.Kind,
|
|
2468
|
+
slotPlacement: slot.Placement,
|
|
2469
|
+
},
|
|
2470
|
+
interpretation: {
|
|
2471
|
+
status: 'bound',
|
|
2472
|
+
target: 'collection-chrome-slot-renderer-registry',
|
|
2473
|
+
},
|
|
2474
|
+
message:
|
|
2475
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' is bound ` +
|
|
2476
|
+
'by the collection chrome slot renderer registry.',
|
|
2477
|
+
reason: `bound-collection-chrome-slot-renderer.${slot.Id}`,
|
|
2478
|
+
severity: 'info',
|
|
2479
|
+
slot,
|
|
2480
|
+
view: viewTrace,
|
|
2481
|
+
}),
|
|
2482
|
+
]
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
if (isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.custom)) {
|
|
2486
|
+
return [
|
|
2487
|
+
createCollectionChromeSlotDiagnostic({
|
|
2488
|
+
category: 'missing-binding',
|
|
2489
|
+
details: {
|
|
2490
|
+
slotId: slot.Id,
|
|
2491
|
+
slotKind: slot.Kind,
|
|
2492
|
+
slotPlacement: slot.Placement,
|
|
2493
|
+
},
|
|
2494
|
+
interpretation: {
|
|
2495
|
+
status: 'unbound',
|
|
2496
|
+
target: 'projected-collection-runtime.chrome.custom-slot',
|
|
2497
|
+
},
|
|
2498
|
+
message:
|
|
2499
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' is Custom, ` +
|
|
2500
|
+
'but no frontend collection chrome-slot interpreter is registered for it.',
|
|
2501
|
+
reason: `unbound-collection-chrome-custom-slot.${slot.Id}`,
|
|
2502
|
+
severity: 'warning',
|
|
2503
|
+
slot,
|
|
2504
|
+
suggestedNextStep:
|
|
2505
|
+
'Register a collection chrome-slot renderer for this custom slot or model it with a first-class slot kind.',
|
|
2506
|
+
view: viewTrace,
|
|
2507
|
+
}),
|
|
2508
|
+
]
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
if (
|
|
2512
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.pagination) &&
|
|
2513
|
+
!isCollectionChromeSlotPlacement(slot, collectionChromeSlotPlacements.footer)
|
|
2514
|
+
) {
|
|
2515
|
+
return [
|
|
2516
|
+
createCollectionChromeSlotDiagnostic({
|
|
2517
|
+
category: 'missing-binding',
|
|
2518
|
+
details: {
|
|
2519
|
+
slotId: slot.Id,
|
|
2520
|
+
slotKind: slot.Kind,
|
|
2521
|
+
slotPlacement: slot.Placement,
|
|
2522
|
+
},
|
|
2523
|
+
interpretation: {
|
|
2524
|
+
status: 'unbound',
|
|
2525
|
+
target: 'projected-collection-runtime.chrome.pagination-slot',
|
|
2526
|
+
},
|
|
2527
|
+
message:
|
|
2528
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' places pagination ` +
|
|
2529
|
+
'outside the footer, but the current collection chrome interpreter only binds footer pagination.',
|
|
2530
|
+
reason: `unbound-collection-pagination-slot-placement.${slot.Id}`,
|
|
2531
|
+
severity: 'warning',
|
|
2532
|
+
slot,
|
|
2533
|
+
suggestedNextStep:
|
|
2534
|
+
'Add a collection chrome-slot renderer for this pagination placement or use Footer placement.',
|
|
2535
|
+
view: viewTrace,
|
|
2536
|
+
}),
|
|
2537
|
+
]
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
if (isCollectionChromeSlotRendererExpected(slot)) {
|
|
2541
|
+
return [
|
|
2542
|
+
createCollectionChromeSlotDiagnostic({
|
|
2543
|
+
category: 'missing-binding',
|
|
2544
|
+
details: {
|
|
2545
|
+
candidateRendererKeys,
|
|
2546
|
+
slotId: slot.Id,
|
|
2547
|
+
slotKind: slot.Kind,
|
|
2548
|
+
slotPlacement: slot.Placement,
|
|
2549
|
+
},
|
|
2550
|
+
interpretation: {
|
|
2551
|
+
status: 'unbound',
|
|
2552
|
+
target: 'collection-chrome-slot-renderer-registry',
|
|
2553
|
+
},
|
|
2554
|
+
message:
|
|
2555
|
+
`Collection chrome slot '${slot.Id}' on '${view.Name}' has no ` +
|
|
2556
|
+
'registered frontend renderer for its kind and placement.',
|
|
2557
|
+
reason: `unbound-collection-chrome-slot-renderer.${slot.Id}`,
|
|
2558
|
+
severity: 'warning',
|
|
2559
|
+
slot,
|
|
2560
|
+
suggestedNextStep:
|
|
2561
|
+
'Register a collection chrome-slot renderer or add a generic interpreter for this slot kind.',
|
|
2562
|
+
view: viewTrace,
|
|
2563
|
+
}),
|
|
2564
|
+
]
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
return []
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function isCollectionChromeSlotRendererExpected(
|
|
2571
|
+
slot: CollectionChromeSlotDefinition,
|
|
2572
|
+
) {
|
|
2573
|
+
return isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.queryForm) ||
|
|
2574
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.pagination) ||
|
|
2575
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.selectionActions) ||
|
|
2576
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.rowActions) ||
|
|
2577
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.detail) ||
|
|
2578
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.summary) ||
|
|
2579
|
+
isCollectionChromeSlotKind(slot, collectionChromeSlotKinds.body)
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
function createCollectionChromeSlotDiagnostic({
|
|
2583
|
+
category,
|
|
2584
|
+
details,
|
|
2585
|
+
interpretation,
|
|
2586
|
+
message,
|
|
2587
|
+
reason,
|
|
2588
|
+
severity,
|
|
2589
|
+
slot,
|
|
2590
|
+
suggestedNextStep,
|
|
2591
|
+
view,
|
|
2592
|
+
}: {
|
|
2593
|
+
readonly category?: PresentationProjectionDiagnostic['category']
|
|
2594
|
+
readonly details?: Readonly<Record<string, unknown>>
|
|
2595
|
+
readonly interpretation?: PresentationProjectionDiagnostic['interpretation']
|
|
2596
|
+
readonly message: string
|
|
2597
|
+
readonly reason: string
|
|
2598
|
+
readonly severity: PresentationProjectionDiagnostic['severity']
|
|
2599
|
+
readonly slot: CollectionChromeSlotDefinition
|
|
2600
|
+
readonly suggestedNextStep?: string
|
|
2601
|
+
readonly view: PresentationProjectionTraceView
|
|
2602
|
+
}) {
|
|
2603
|
+
return createViewDiagnostic({
|
|
2604
|
+
category,
|
|
2605
|
+
details: {
|
|
2606
|
+
collectionChromeSlotId: slot.Id,
|
|
2607
|
+
collectionChromeSlotKind: slot.Kind,
|
|
2608
|
+
collectionChromeSlotPlacement: slot.Placement,
|
|
2609
|
+
...details,
|
|
2610
|
+
},
|
|
2611
|
+
interpretation,
|
|
2612
|
+
message,
|
|
2613
|
+
reason,
|
|
2614
|
+
severity,
|
|
2615
|
+
source: 'presentation-collection-chrome-coverage',
|
|
2616
|
+
suggestedNextStep,
|
|
2617
|
+
view,
|
|
2618
|
+
})
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
function projectCollectionRowActionPredicateCoverageDiagnostics(
|
|
2622
|
+
rowAction: CollectionRowActionDefinition,
|
|
2623
|
+
view: ViewDefinition,
|
|
2624
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2625
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2626
|
+
return [
|
|
2627
|
+
...projectCollectionRowActionPredicateDiagnostic({
|
|
2628
|
+
predicate: rowAction.IsEnabled,
|
|
2629
|
+
predicateName: 'IsEnabled',
|
|
2630
|
+
rowAction,
|
|
2631
|
+
view,
|
|
2632
|
+
viewTrace,
|
|
2633
|
+
}),
|
|
2634
|
+
...projectCollectionRowActionPredicateDiagnostic({
|
|
2635
|
+
predicate: rowAction.IsVisible,
|
|
2636
|
+
predicateName: 'IsVisible',
|
|
2637
|
+
rowAction,
|
|
2638
|
+
view,
|
|
2639
|
+
viewTrace,
|
|
2640
|
+
}),
|
|
2641
|
+
]
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
function projectCollectionSelectionActionPredicateCoverageDiagnostics(
|
|
2645
|
+
selectionAction: CollectionSelectionActionDefinition,
|
|
2646
|
+
view: ViewDefinition,
|
|
2647
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2648
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2649
|
+
return [
|
|
2650
|
+
...projectCollectionSelectionActionPredicateDiagnostic({
|
|
2651
|
+
predicate: selectionAction.IsEnabled,
|
|
2652
|
+
predicateName: 'IsEnabled',
|
|
2653
|
+
selectionAction,
|
|
2654
|
+
view,
|
|
2655
|
+
viewTrace,
|
|
2656
|
+
}),
|
|
2657
|
+
...projectCollectionSelectionActionPredicateDiagnostic({
|
|
2658
|
+
predicate: selectionAction.IsVisible,
|
|
2659
|
+
predicateName: 'IsVisible',
|
|
2660
|
+
selectionAction,
|
|
2661
|
+
view,
|
|
2662
|
+
viewTrace,
|
|
2663
|
+
}),
|
|
2664
|
+
]
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function projectCollectionRowActionPredicateDiagnostic({
|
|
2668
|
+
predicate,
|
|
2669
|
+
predicateName,
|
|
2670
|
+
rowAction,
|
|
2671
|
+
view,
|
|
2672
|
+
viewTrace,
|
|
2673
|
+
}: {
|
|
2674
|
+
readonly predicate: PresentationValueDefinition | null | undefined
|
|
2675
|
+
readonly predicateName: 'IsEnabled' | 'IsVisible'
|
|
2676
|
+
readonly rowAction: CollectionRowActionDefinition
|
|
2677
|
+
readonly view: ViewDefinition
|
|
2678
|
+
readonly viewTrace: PresentationProjectionTraceView
|
|
2679
|
+
}): readonly PresentationProjectionDiagnostic[] {
|
|
2680
|
+
if (!predicate || isInterpretableCollectionRowActionPredicate(predicate)) {
|
|
2681
|
+
return []
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
return [
|
|
2685
|
+
createViewDiagnostic({
|
|
2686
|
+
category: 'missing-binding',
|
|
2687
|
+
details: {
|
|
2688
|
+
actionId: rowAction.ActionId,
|
|
2689
|
+
expression: predicate.Expression,
|
|
2690
|
+
field: predicate.Field,
|
|
2691
|
+
predicateKind: predicate.Kind,
|
|
2692
|
+
predicateName,
|
|
2693
|
+
rowActionId: rowAction.Id,
|
|
2694
|
+
stateId: predicate.StateId,
|
|
2695
|
+
},
|
|
2696
|
+
interpretation: {
|
|
2697
|
+
status: 'unbound',
|
|
2698
|
+
target: 'collection-row-action-predicate',
|
|
2699
|
+
},
|
|
2700
|
+
message:
|
|
2701
|
+
`Collection row action '${rowAction.Id}' on '${view.Name}' declares ` +
|
|
2702
|
+
`${predicateName}, but the frontend collection interpreter only binds literal and row-field predicates.`,
|
|
2703
|
+
reason: `unbound-collection-row-action-predicate.${rowAction.Id}.${predicateName}`,
|
|
2704
|
+
severity: 'warning',
|
|
2705
|
+
source: 'presentation-collection-coverage',
|
|
2706
|
+
suggestedNextStep:
|
|
2707
|
+
'Bind this predicate through a target expression interpreter or express it as a literal/row-field predicate.',
|
|
2708
|
+
view: viewTrace,
|
|
2709
|
+
}),
|
|
2710
|
+
]
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
function projectCollectionSelectionActionPredicateDiagnostic({
|
|
2714
|
+
predicate,
|
|
2715
|
+
predicateName,
|
|
2716
|
+
selectionAction,
|
|
2717
|
+
view,
|
|
2718
|
+
viewTrace,
|
|
2719
|
+
}: {
|
|
2720
|
+
readonly predicate: PresentationValueDefinition | null | undefined
|
|
2721
|
+
readonly predicateName: 'IsEnabled' | 'IsVisible'
|
|
2722
|
+
readonly selectionAction: CollectionSelectionActionDefinition
|
|
2723
|
+
readonly view: ViewDefinition
|
|
2724
|
+
readonly viewTrace: PresentationProjectionTraceView
|
|
2725
|
+
}): readonly PresentationProjectionDiagnostic[] {
|
|
2726
|
+
if (!predicate || isInterpretableCollectionRowActionPredicate(predicate)) {
|
|
2727
|
+
return []
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
return [
|
|
2731
|
+
createViewDiagnostic({
|
|
2732
|
+
category: 'missing-binding',
|
|
2733
|
+
details: {
|
|
2734
|
+
actionId: selectionAction.ActionId,
|
|
2735
|
+
expression: predicate.Expression,
|
|
2736
|
+
field: predicate.Field,
|
|
2737
|
+
predicateKind: predicate.Kind,
|
|
2738
|
+
predicateName,
|
|
2739
|
+
selectionActionId: selectionAction.Id,
|
|
2740
|
+
stateId: predicate.StateId,
|
|
2741
|
+
},
|
|
2742
|
+
interpretation: {
|
|
2743
|
+
status: 'unbound',
|
|
2744
|
+
target: 'collection-selection-action-predicate',
|
|
2745
|
+
},
|
|
2746
|
+
message:
|
|
2747
|
+
`Collection selection action '${selectionAction.Id}' on '${view.Name}' declares ` +
|
|
2748
|
+
`${predicateName}, but the frontend collection interpreter only binds literal and selected-row-field predicates.`,
|
|
2749
|
+
reason: `unbound-collection-selection-action-predicate.${selectionAction.Id}.${predicateName}`,
|
|
2750
|
+
severity: 'warning',
|
|
2751
|
+
source: 'presentation-collection-coverage',
|
|
2752
|
+
suggestedNextStep:
|
|
2753
|
+
'Bind this predicate through a target expression interpreter or express it as a literal/selected-row-field predicate.',
|
|
2754
|
+
view: viewTrace,
|
|
2755
|
+
}),
|
|
2756
|
+
]
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
function isCollectionSelectionEnabled(selectionMode: unknown) {
|
|
2760
|
+
return selectionMode === collectionSelectionModes.single ||
|
|
2761
|
+
selectionMode === collectionSelectionModes.multiple ||
|
|
2762
|
+
String(selectionMode).toLocaleLowerCase() === 'single' ||
|
|
2763
|
+
String(selectionMode).toLocaleLowerCase() === 'multiple'
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
function isInterpretableCollectionRowActionPredicate(predicate: PresentationValueDefinition) {
|
|
2767
|
+
return predicate.Kind === presentationValueKinds.literal ||
|
|
2768
|
+
predicate.Kind === presentationValueKinds.field ||
|
|
2769
|
+
String(predicate.Kind).toLocaleLowerCase() === 'literal' ||
|
|
2770
|
+
String(predicate.Kind).toLocaleLowerCase() === 'field'
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
function isSelectedRowValueSelectionActionParameter(source: unknown) {
|
|
2774
|
+
return source === collectionSelectionActionParameterSources.selectedRowValue ||
|
|
2775
|
+
source === collectionSelectionActionParameterSources.selectedRowValueList ||
|
|
2776
|
+
String(source).toLocaleLowerCase() === 'selectedrowvalue' ||
|
|
2777
|
+
String(source).toLocaleLowerCase() === 'selectedrowvaluelist'
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function isCollectionDetailActivationUnsupported(detailActivation: unknown) {
|
|
2781
|
+
return detailActivation === collectionDetailActivations.none ||
|
|
2782
|
+
detailActivation === collectionDetailActivations.hover ||
|
|
2783
|
+
String(detailActivation).toLocaleLowerCase() === 'none' ||
|
|
2784
|
+
String(detailActivation).toLocaleLowerCase() === 'hover'
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
function isCollectionDetailActivationNone(detailActivation: unknown) {
|
|
2788
|
+
return detailActivation === collectionDetailActivations.none ||
|
|
2789
|
+
String(detailActivation).toLocaleLowerCase() === 'none'
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
function isCollectionDetailActivationSelection(detailActivation: unknown) {
|
|
2793
|
+
return detailActivation === collectionDetailActivations.selection ||
|
|
2794
|
+
String(detailActivation).toLocaleLowerCase() === 'selection'
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
function isCollectionDetailActivationStateful(detailActivation: unknown) {
|
|
2798
|
+
return isCollectionDetailActivationSelection(detailActivation) ||
|
|
2799
|
+
detailActivation === collectionDetailActivations.rowActivation ||
|
|
2800
|
+
String(detailActivation).toLocaleLowerCase() === 'rowactivation'
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
function isPrimaryCollectionRowAction(rowAction: CollectionRowActionDefinition) {
|
|
2804
|
+
return rowAction.Kind === collectionRowActionKinds.primary ||
|
|
2805
|
+
String(rowAction.Kind).toLocaleLowerCase() === 'primary'
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
function isCollectionQueryDataSource(
|
|
2809
|
+
dataSource: PresentationModuleDefinition['DataSources'][number],
|
|
2810
|
+
) {
|
|
2811
|
+
return dataSource.Kind === dataSourceKinds.collectionQuery ||
|
|
2812
|
+
String(dataSource.Kind).toLocaleLowerCase() === 'collectionquery'
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function resolveViewProjectedFieldRefs(view: ViewDefinition) {
|
|
2816
|
+
const refs = getPresentationViewProjectedFieldRefs(view)
|
|
2817
|
+
const seen = new Set<string>()
|
|
2818
|
+
return refs.filter((ref) => {
|
|
2819
|
+
const key = `${ref.source}:${ref.fieldId}`
|
|
2820
|
+
if (seen.has(key)) {
|
|
2821
|
+
return false
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
seen.add(key)
|
|
2825
|
+
return true
|
|
2826
|
+
})
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
function projectActionCoverageDiagnostics(
|
|
2830
|
+
module: PresentationModuleDefinition,
|
|
2831
|
+
view: ViewDefinition,
|
|
2832
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2833
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2834
|
+
return getPresentationViewProjectedActions(view).flatMap((actionRef) =>
|
|
2835
|
+
projectActionPlacementCoverageDiagnostics(module, actionRef.placement, viewTrace, {
|
|
2836
|
+
actionKind: actionRef.kind,
|
|
2837
|
+
actionContextKind: actionRef.contextKind,
|
|
2838
|
+
actionSource: actionRef.source,
|
|
2839
|
+
collectionSlotId: actionRef.slotId ?? null,
|
|
2840
|
+
}),
|
|
2841
|
+
)
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function projectActionPlacementCoverageDiagnostics(
|
|
2845
|
+
module: PresentationModuleDefinition,
|
|
2846
|
+
placement: ActionPlacementDefinition,
|
|
2847
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2848
|
+
details?: Readonly<Record<string, unknown>>,
|
|
2849
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2850
|
+
const action = findPresentationAction<ActionDefinition>(module, placement.ActionId)
|
|
2851
|
+
if (!action) {
|
|
2852
|
+
return [
|
|
2853
|
+
createViewDiagnostic({
|
|
2854
|
+
details: {
|
|
2855
|
+
actionId: placement.ActionId,
|
|
2856
|
+
...details,
|
|
2857
|
+
placementRegion: placement.Region,
|
|
2858
|
+
},
|
|
2859
|
+
message: `View '${viewTrace.name}' places action '${placement.ActionId}', but the action is not present in the action catalog.`,
|
|
2860
|
+
reason: `missing-action.${placement.ActionId}`,
|
|
2861
|
+
severity: 'error',
|
|
2862
|
+
source: 'presentation-action-coverage',
|
|
2863
|
+
view: viewTrace,
|
|
2864
|
+
}),
|
|
2865
|
+
]
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
const diagnostics: PresentationProjectionDiagnostic[] = []
|
|
2869
|
+
const flowId = action.Preparation?.FlowId
|
|
2870
|
+
if (flowId && !findPresentationFlow(module, flowId)) {
|
|
2871
|
+
diagnostics.push(createViewDiagnostic({
|
|
2872
|
+
details: {
|
|
2873
|
+
actionId: action.Id,
|
|
2874
|
+
flowId,
|
|
2875
|
+
},
|
|
2876
|
+
message: `Action '${action.Name}' references flow '${flowId}', but that flow is not present in the presentation module.`,
|
|
2877
|
+
reason: `missing-action-flow.${action.Id}.${flowId}`,
|
|
2878
|
+
severity: 'error',
|
|
2879
|
+
source: 'presentation-action-coverage',
|
|
2880
|
+
view: viewTrace,
|
|
2881
|
+
}))
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
for (const request of action.EndpointRequests) {
|
|
2885
|
+
if (
|
|
2886
|
+
request.DataSourceId &&
|
|
2887
|
+
!findPresentationDataSource(module, request.DataSourceId)
|
|
2888
|
+
) {
|
|
2889
|
+
diagnostics.push(createViewDiagnostic({
|
|
2890
|
+
details: {
|
|
2891
|
+
actionId: action.Id,
|
|
2892
|
+
dataSourceId: request.DataSourceId,
|
|
2893
|
+
},
|
|
2894
|
+
message: `Action '${action.Name}' references data source '${request.DataSourceId}', but that data source is not present in the presentation module.`,
|
|
2895
|
+
reason: `missing-action-data-source.${action.Id}.${request.DataSourceId}`,
|
|
2896
|
+
severity: 'error',
|
|
2897
|
+
source: 'presentation-action-coverage',
|
|
2898
|
+
view: viewTrace,
|
|
2899
|
+
}))
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
if (
|
|
2904
|
+
isLocalStateAction(action) &&
|
|
2905
|
+
!action.Semantics &&
|
|
2906
|
+
!action.Annotations.some((annotation) =>
|
|
2907
|
+
String(annotation.Name).toLocaleLowerCase().includes('frontend'))
|
|
2908
|
+
) {
|
|
2909
|
+
diagnostics.push(createViewDiagnostic({
|
|
2910
|
+
category: 'local-interpretation',
|
|
2911
|
+
details: {
|
|
2912
|
+
actionId: action.Id,
|
|
2913
|
+
bindingKind: action.Binding.Kind,
|
|
2914
|
+
},
|
|
2915
|
+
interpretation: {
|
|
2916
|
+
status: 'locally-interpreted',
|
|
2917
|
+
target: 'local-action-runtime',
|
|
2918
|
+
},
|
|
2919
|
+
message: `Local action '${action.Name}' has no first-class semantics; it requires an implicit frontend binding.`,
|
|
2920
|
+
reason: `local-action-without-semantics.${action.Id}`,
|
|
2921
|
+
severity: 'info',
|
|
2922
|
+
source: 'presentation-action-coverage',
|
|
2923
|
+
suggestedNextStep:
|
|
2924
|
+
'Declare action semantics in the IR and leave only the component-specific interpretation in the frontend adapter.',
|
|
2925
|
+
view: viewTrace,
|
|
2926
|
+
}))
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
return diagnostics
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
function projectInputFormCoverageDiagnostics(
|
|
2933
|
+
module: PresentationModuleDefinition,
|
|
2934
|
+
view: ViewDefinition,
|
|
2935
|
+
viewTrace: PresentationProjectionTraceView,
|
|
2936
|
+
queryFormStateAdapterIds: readonly string[],
|
|
2937
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
2938
|
+
const inputFormRefs = resolveViewInputFormRefs(module, view)
|
|
2939
|
+
return inputFormRefs.flatMap(({ form, formId, queryFormId, source }) => {
|
|
2940
|
+
if (!form) {
|
|
2941
|
+
return [
|
|
2942
|
+
createViewDiagnostic({
|
|
2943
|
+
details: { formId, source },
|
|
2944
|
+
message: `View '${view.Name}' references input form '${formId}', but that form is not present in the presentation module.`,
|
|
2945
|
+
reason: `missing-input-form.${formId}`,
|
|
2946
|
+
severity: 'error',
|
|
2947
|
+
source: 'presentation-input-form-coverage',
|
|
2948
|
+
view: viewTrace,
|
|
2949
|
+
}),
|
|
2950
|
+
]
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
return [
|
|
2954
|
+
...projectInputFormFieldCoverageDiagnostics(module, form, viewTrace),
|
|
2955
|
+
...projectInputFormActionCoverageDiagnostics(module, form, viewTrace),
|
|
2956
|
+
...projectQueryFormStateAdapterCoverageDiagnostics({
|
|
2957
|
+
queryFormId,
|
|
2958
|
+
queryFormStateAdapterIds,
|
|
2959
|
+
viewTrace,
|
|
2960
|
+
}),
|
|
2961
|
+
]
|
|
2962
|
+
})
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
function resolveViewInputFormRefs(
|
|
2966
|
+
module: PresentationModuleDefinition,
|
|
2967
|
+
view: ViewDefinition,
|
|
2968
|
+
): readonly {
|
|
2969
|
+
readonly form: InputFormDefinition | null
|
|
2970
|
+
readonly formId: string
|
|
2971
|
+
readonly queryFormId: string | null
|
|
2972
|
+
readonly source: string
|
|
2973
|
+
}[] {
|
|
2974
|
+
const directInputFormId = view.Subject.InputFormId
|
|
2975
|
+
if (directInputFormId) {
|
|
2976
|
+
return [{
|
|
2977
|
+
form: findPresentationInputForm<InputFormDefinition>(module, directInputFormId),
|
|
2978
|
+
formId: directInputFormId,
|
|
2979
|
+
queryFormId: null,
|
|
2980
|
+
source: 'view-subject',
|
|
2981
|
+
}]
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
const queryFormRefs = [
|
|
2985
|
+
...(view.Subject.QueryFormId
|
|
2986
|
+
? [{ queryFormId: view.Subject.QueryFormId, source: 'view-subject' }]
|
|
2987
|
+
: []),
|
|
2988
|
+
...createCollectionChromeRuntime(view.Collection)
|
|
2989
|
+
.findSlots(collectionChromeSlotKinds.queryForm)
|
|
2990
|
+
.flatMap((slot) =>
|
|
2991
|
+
slot.QueryFormId
|
|
2992
|
+
? [{
|
|
2993
|
+
queryFormId: slot.QueryFormId,
|
|
2994
|
+
source: `collection-chrome-slot:${slot.Id}`,
|
|
2995
|
+
}]
|
|
2996
|
+
: []),
|
|
2997
|
+
]
|
|
2998
|
+
const seenQueryFormIds = new Set<string>()
|
|
2999
|
+
|
|
3000
|
+
return queryFormRefs.flatMap(({ queryFormId, source }) => {
|
|
3001
|
+
if (seenQueryFormIds.has(queryFormId)) {
|
|
3002
|
+
return []
|
|
3003
|
+
}
|
|
3004
|
+
seenQueryFormIds.add(queryFormId)
|
|
3005
|
+
|
|
3006
|
+
const queryForm = findPresentationQueryForm<QueryFormDefinition>(module, queryFormId)
|
|
3007
|
+
if (!queryForm) {
|
|
3008
|
+
return [{
|
|
3009
|
+
form: null,
|
|
3010
|
+
formId: queryFormId,
|
|
3011
|
+
queryFormId,
|
|
3012
|
+
source,
|
|
3013
|
+
}]
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
return [{
|
|
3017
|
+
form: findPresentationInputForm<InputFormDefinition>(module, queryForm.FormId),
|
|
3018
|
+
formId: queryForm.FormId,
|
|
3019
|
+
queryFormId,
|
|
3020
|
+
source,
|
|
3021
|
+
}]
|
|
3022
|
+
})
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
function projectInputFormFieldCoverageDiagnostics(
|
|
3026
|
+
module: PresentationModuleDefinition,
|
|
3027
|
+
form: InputFormDefinition,
|
|
3028
|
+
viewTrace: PresentationProjectionTraceView,
|
|
3029
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
3030
|
+
const inputFieldIds = new Set(form.Fields.map((field) => field.Id))
|
|
3031
|
+
const diagnostics = form.Fields.flatMap((field) => {
|
|
3032
|
+
const presentationField = findPresentationField(module, field.FieldId)
|
|
3033
|
+
const choiceDataSourceId = field.ChoiceSource?.DataSourceId
|
|
3034
|
+
return [
|
|
3035
|
+
...(presentationField
|
|
3036
|
+
? []
|
|
3037
|
+
: [
|
|
3038
|
+
createViewDiagnostic({
|
|
3039
|
+
details: {
|
|
3040
|
+
fieldId: field.FieldId,
|
|
3041
|
+
formId: form.Id,
|
|
3042
|
+
inputFieldId: field.Id,
|
|
3043
|
+
},
|
|
3044
|
+
message: `Input form '${form.Name}' references field '${field.FieldId}', but that field is not present in the presentation module.`,
|
|
3045
|
+
reason: `missing-input-form-field.${form.Id}.${field.Id}`,
|
|
3046
|
+
severity: 'error' as const,
|
|
3047
|
+
source: 'presentation-input-form-coverage',
|
|
3048
|
+
view: viewTrace,
|
|
3049
|
+
}),
|
|
3050
|
+
]),
|
|
3051
|
+
...(choiceDataSourceId && !findPresentationDataSource(module, choiceDataSourceId)
|
|
3052
|
+
? [
|
|
3053
|
+
createViewDiagnostic({
|
|
3054
|
+
details: {
|
|
3055
|
+
choiceDataSourceId,
|
|
3056
|
+
formId: form.Id,
|
|
3057
|
+
inputFieldId: field.Id,
|
|
3058
|
+
},
|
|
3059
|
+
message: `Input form '${form.Name}' field '${field.Name}' references choice data source '${choiceDataSourceId}', but that data source is not present in the presentation module.`,
|
|
3060
|
+
reason: `missing-input-form-choice-data-source.${form.Id}.${field.Id}`,
|
|
3061
|
+
severity: 'error' as const,
|
|
3062
|
+
source: 'presentation-input-form-coverage',
|
|
3063
|
+
view: viewTrace,
|
|
3064
|
+
}),
|
|
3065
|
+
]
|
|
3066
|
+
: []),
|
|
3067
|
+
]
|
|
3068
|
+
})
|
|
3069
|
+
|
|
3070
|
+
return [
|
|
3071
|
+
...diagnostics,
|
|
3072
|
+
...form.Groups.flatMap((group) =>
|
|
3073
|
+
group.FieldIds.filter((fieldId) => !inputFieldIds.has(fieldId)).map((fieldId) =>
|
|
3074
|
+
createViewDiagnostic({
|
|
3075
|
+
details: {
|
|
3076
|
+
formId: form.Id,
|
|
3077
|
+
groupId: group.Id,
|
|
3078
|
+
inputFieldId: fieldId,
|
|
3079
|
+
},
|
|
3080
|
+
message: `Input form '${form.Name}' group '${group.Name}' references field '${fieldId}', but that input field is not present in the form.`,
|
|
3081
|
+
reason: `missing-input-form-group-field.${form.Id}.${group.Id}.${fieldId}`,
|
|
3082
|
+
severity: 'error',
|
|
3083
|
+
source: 'presentation-input-form-coverage',
|
|
3084
|
+
view: viewTrace,
|
|
3085
|
+
}),
|
|
3086
|
+
),
|
|
3087
|
+
),
|
|
3088
|
+
]
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
function projectInputFormActionCoverageDiagnostics(
|
|
3092
|
+
module: PresentationModuleDefinition,
|
|
3093
|
+
form: InputFormDefinition,
|
|
3094
|
+
viewTrace: PresentationProjectionTraceView,
|
|
3095
|
+
): readonly PresentationProjectionDiagnostic[] {
|
|
3096
|
+
return form.Actions.flatMap((placement) =>
|
|
3097
|
+
findPresentationAction(module, placement.ActionId)
|
|
3098
|
+
? []
|
|
3099
|
+
: [
|
|
3100
|
+
createViewDiagnostic({
|
|
3101
|
+
details: {
|
|
3102
|
+
actionId: placement.ActionId,
|
|
3103
|
+
formId: form.Id,
|
|
3104
|
+
placementRegion: placement.Region,
|
|
3105
|
+
},
|
|
3106
|
+
message: `Input form '${form.Name}' places action '${placement.ActionId}', but the action is not present in the action catalog.`,
|
|
3107
|
+
reason: `missing-input-form-action.${form.Id}.${placement.ActionId}`,
|
|
3108
|
+
severity: 'error',
|
|
3109
|
+
source: 'presentation-input-form-coverage',
|
|
3110
|
+
view: viewTrace,
|
|
3111
|
+
}),
|
|
3112
|
+
],
|
|
3113
|
+
)
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
function projectQueryFormStateAdapterCoverageDiagnostics({
|
|
3117
|
+
queryFormId,
|
|
3118
|
+
queryFormStateAdapterIds,
|
|
3119
|
+
viewTrace,
|
|
3120
|
+
}: {
|
|
3121
|
+
readonly queryFormId: string | null
|
|
3122
|
+
readonly queryFormStateAdapterIds: readonly string[]
|
|
3123
|
+
readonly viewTrace: PresentationProjectionTraceView
|
|
3124
|
+
}): readonly PresentationProjectionDiagnostic[] {
|
|
3125
|
+
if (!queryFormId || queryFormStateAdapterIds.includes(queryFormId)) {
|
|
3126
|
+
return []
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
return [
|
|
3130
|
+
createViewDiagnostic({
|
|
3131
|
+
category: 'missing-binding',
|
|
3132
|
+
details: { queryFormId },
|
|
3133
|
+
interpretation: {
|
|
3134
|
+
status: 'unbound',
|
|
3135
|
+
target: 'query-form-state-adapter',
|
|
3136
|
+
},
|
|
3137
|
+
message:
|
|
3138
|
+
`Query form '${queryFormId}' is projected into view '${viewTrace.name}', ` +
|
|
3139
|
+
'but no frontend state adapter is registered for it.',
|
|
3140
|
+
reason: `missing-query-form-state-adapter.${queryFormId}`,
|
|
3141
|
+
severity: 'warning',
|
|
3142
|
+
source: 'presentation-query-form-coverage',
|
|
3143
|
+
suggestedNextStep:
|
|
3144
|
+
'Register a query-form state adapter or project a generic relation-query state runtime for this form.',
|
|
3145
|
+
view: viewTrace,
|
|
3146
|
+
}),
|
|
3147
|
+
]
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function isLocalStateAction(action: ActionDefinition) {
|
|
3151
|
+
return (
|
|
3152
|
+
action.Kind === actionKinds.localStateAction ||
|
|
3153
|
+
String(action.Kind).toLocaleLowerCase() === 'localstateaction' ||
|
|
3154
|
+
action.Binding.Kind === presentationBindingKinds.localState ||
|
|
3155
|
+
String(action.Binding.Kind).toLocaleLowerCase() === 'localstate'
|
|
3156
|
+
)
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
function isUnboundDataSourceBinding(binding: PresentationDataSourceBinding) {
|
|
3160
|
+
const authorization = binding.authorization
|
|
3161
|
+
const blockedLabel =
|
|
3162
|
+
authorization.kind === 'required'
|
|
3163
|
+
? authorization.blockedLabel ?? binding.blockedLabel
|
|
3164
|
+
: binding.blockedLabel
|
|
3165
|
+
|
|
3166
|
+
return (
|
|
3167
|
+
binding.kind === presentationDataSourceBindingKinds.localValue &&
|
|
3168
|
+
authorization.kind === 'required' &&
|
|
3169
|
+
authorization.isAuthorized === false &&
|
|
3170
|
+
Boolean(blockedLabel?.startsWith('No frontend binding is registered'))
|
|
3171
|
+
)
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
function createViewDiagnostic({
|
|
3175
|
+
category,
|
|
3176
|
+
details,
|
|
3177
|
+
interpretation,
|
|
3178
|
+
message,
|
|
3179
|
+
reason,
|
|
3180
|
+
severity,
|
|
3181
|
+
source,
|
|
3182
|
+
suggestedNextStep,
|
|
3183
|
+
view,
|
|
3184
|
+
}: {
|
|
3185
|
+
readonly category?: PresentationProjectionDiagnostic['category']
|
|
3186
|
+
readonly details?: Readonly<Record<string, unknown>>
|
|
3187
|
+
readonly interpretation?: PresentationProjectionDiagnostic['interpretation']
|
|
3188
|
+
readonly message: string
|
|
3189
|
+
readonly reason: string
|
|
3190
|
+
readonly severity: PresentationProjectionDiagnostic['severity']
|
|
3191
|
+
readonly source: string
|
|
3192
|
+
readonly suggestedNextStep?: string
|
|
3193
|
+
readonly view: PresentationProjectionTraceView
|
|
3194
|
+
}) {
|
|
3195
|
+
return createDiagnostic({
|
|
3196
|
+
category,
|
|
3197
|
+
details,
|
|
3198
|
+
id: `view.${view.id}.${reason}`,
|
|
3199
|
+
interpretation,
|
|
3200
|
+
message,
|
|
3201
|
+
severity,
|
|
3202
|
+
source,
|
|
3203
|
+
subject: {
|
|
3204
|
+
id: view.id,
|
|
3205
|
+
kind: 'view',
|
|
3206
|
+
name: view.name,
|
|
3207
|
+
},
|
|
3208
|
+
suggestedNextStep,
|
|
3209
|
+
})
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
function createDiagnostic({
|
|
3213
|
+
category,
|
|
3214
|
+
details,
|
|
3215
|
+
id,
|
|
3216
|
+
interpretation,
|
|
3217
|
+
message,
|
|
3218
|
+
severity,
|
|
3219
|
+
source,
|
|
3220
|
+
subject,
|
|
3221
|
+
suggestedNextStep,
|
|
3222
|
+
}: PresentationProjectionDiagnostic): PresentationProjectionDiagnostic {
|
|
3223
|
+
return {
|
|
3224
|
+
category,
|
|
3225
|
+
details,
|
|
3226
|
+
id,
|
|
3227
|
+
interpretation,
|
|
3228
|
+
message,
|
|
3229
|
+
severity,
|
|
3230
|
+
source,
|
|
3231
|
+
subject,
|
|
3232
|
+
suggestedNextStep,
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
function normalizeCoverageModule(
|
|
3237
|
+
module: PresentationProjectionCoverageModule,
|
|
3238
|
+
): PresentationModuleDefinition {
|
|
3239
|
+
return {
|
|
3240
|
+
Actions: [...(module.Actions ?? [])],
|
|
3241
|
+
Annotations: [],
|
|
3242
|
+
DataSources: [...(module.DataSources ?? [])],
|
|
3243
|
+
DesignSystems: [],
|
|
3244
|
+
Expressions: [],
|
|
3245
|
+
Fields: [...(module.Fields ?? [])],
|
|
3246
|
+
Flows: [...(module.Flows ?? [])],
|
|
3247
|
+
Id: 'projection-coverage',
|
|
3248
|
+
InputForms: [...(module.InputForms ?? [])],
|
|
3249
|
+
Name: 'Projection Coverage',
|
|
3250
|
+
Navigation: [],
|
|
3251
|
+
QueryForms: [...(module.QueryForms ?? [])],
|
|
3252
|
+
Targets: [],
|
|
3253
|
+
Version: null,
|
|
3254
|
+
Views: [...module.Views],
|
|
3255
|
+
Workspaces: [],
|
|
3256
|
+
}
|
|
3257
|
+
}
|