@pega/custom-dx-components 26.1.11 → 26.1.12-alpha.0.2
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/README.md +6 -2
- package/package.json +48 -64
- package/src/ai/README.md +44 -0
- package/src/ai/catalog/README.md +133 -0
- package/src/ai/catalog/best-practices/README.md +24 -0
- package/src/ai/catalog/best-practices/accessibility-for-async-components.md +53 -0
- package/src/ai/catalog/best-practices/error-handling-and-resiliency.md +116 -0
- package/src/ai/catalog/best-practices/genai-assistant-integration-patterns.md +49 -0
- package/src/ai/catalog/best-practices/performance-and-efficiency.md +109 -0
- package/src/ai/catalog/best-practices/security-considerations.md +93 -0
- package/src/ai/catalog/best-practices/testing-pconnect-components.md +245 -0
- package/src/ai/catalog/best-practices/troubleshooting-common-pitfalls.md +132 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/SKILL.md +44 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/README.md +39 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/approval-workflow-decision.md +47 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/attachment-upload.md +45 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/bulk-action-submit.md +50 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/confirm-before-destructive-action.md +43 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/create-and-stage-new-case.md +45 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/graceful-submit-failure.md +39 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/modal-assignment-submit.md +58 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/multi-step-wizard.md +44 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/resolve-or-ignore-duplicate.md +47 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/safe-case-deletion.md +36 -0
- package/src/ai/catalog/pconnect-case-collaboration/SKILL.md +42 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/README.md +36 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/look-up-user-details.md +43 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/manage-case-attachments.md +51 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/manage-case-participants.md +52 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/participant-and-follower.md +40 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/post-and-manage-feed.md +50 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/relate-cases.md +35 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/tag-case-and-find-related.md +44 -0
- package/src/ai/catalog/pconnect-data-subscriptions/SKILL.md +42 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/README.md +35 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/data-page-subscription-cleanup.md +41 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/publish-and-subscribe-events.md +40 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/pubsub-cleanup.md +38 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/query-paginated-list-data.md +44 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/realtime-case-subscription.md +46 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/refresh-and-update-data.md +41 -0
- package/src/ai/catalog/pconnect-forms-and-fields/SKILL.md +44 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/README.md +36 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/autosave-tracking.md +45 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/cascading-dependent-fields.md +51 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/collect-form-changes-for-manual-save.md +45 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/custom-field-validators.md +44 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/discard-changes-and-reset-form.md +41 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/full-form-validation.md +38 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/persist-widget-ui-state.md +41 -0
- package/src/ai/catalog/pconnect-lists-and-views/SKILL.md +44 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/README.md +35 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/activate-and-switch-container-items.md +52 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/container-item-lifecycle.md +61 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/insert-delete-list-rows.md +40 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/reorder-list-rows.md +39 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/repeating-row-edit.md +39 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/view-personalization.md +60 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/SKILL.md +45 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/README.md +35 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/agent-conversation-for-tasks.md +54 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/bookmarkable-filter-state.md +40 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/deep-link-semantic-url.md +39 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/genai-assistant-conversation.md +43 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/locale-switch-and-relocalize.md +38 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/persist-operator-preferences.md +40 -0
- package/src/ai/catalog/references/architecture-overview.md +52 -0
- package/src/ai/catalog/references/pconnect-actions-api.md +948 -0
- package/src/ai/catalog/references/pconnect-case-info-data-info-localization.md +618 -0
- package/src/ai/catalog/references/pconnect-case-validation-utilities.md +238 -0
- package/src/ai/catalog/references/pconnect-container-manager.md +215 -0
- package/src/ai/catalog/references/pconnect-list-actions.md +250 -0
- package/src/ai/catalog/references/pconnect-misc-suggestions-creation.md +231 -0
- package/src/ai/catalog/references/pconnect-reference-context-component.md +471 -0
- package/src/ai/catalog/references/pconnect-state-config.md +316 -0
- package/src/ai/catalog/references/pconnect-validation-api.md +45 -0
- package/src/ai/catalog/references/pcore-analytics-genai-details.md +50 -0
- package/src/ai/catalog/references/pcore-bootstrap-init-details.md +121 -0
- package/src/ai/catalog/references/pcore-bootstrap-registry-routing.md +253 -0
- package/src/ai/catalog/references/pcore-case-collaboration-details.md +191 -0
- package/src/ai/catalog/references/pcore-case-collaboration-genai.md +264 -0
- package/src/ai/catalog/references/pcore-environment-info-details.md +96 -0
- package/src/ai/catalog/references/pcore-environment-locale-namespace.md +175 -0
- package/src/ai/catalog/references/pcore-messaging-details.md +99 -0
- package/src/ai/catalog/references/pcore-misc-details.md +207 -0
- package/src/ai/catalog/references/pcore-misc-utilities.md +325 -0
- package/src/ai/catalog/references/pcore-rest-data-details.md +164 -0
- package/src/ai/catalog/references/pcore-rest-data-messaging.md +308 -0
- package/src/ai/catalog/references/pcore-store-context-details.md +40 -0
- package/src/ai/catalog/references/pcore-store-context-state.md +130 -0
- package/src/ai/index.js +154 -0
- package/src/ai/project/.claude/skills/boolean/SKILL.md +43 -0
- package/src/ai/project/.claude/skills/case/SKILL.md +58 -0
- package/src/ai/project/.claude/skills/create-dx-component/SKILL.md +98 -0
- package/src/ai/project/.claude/skills/create-dx-component/assets/PConnProps.d.ts +24 -0
- package/src/ai/project/.claude/skills/create-dx-component/assets/create-nonce.ts +5 -0
- package/src/ai/project/.claude/skills/create-dx-component/assets/event-utils.ts +18 -0
- package/src/ai/project/.claude/skills/create-dx-component/assets/styles.ts +7 -0
- package/src/ai/project/.claude/skills/create-dx-component/cookbooks/README.md +28 -0
- package/src/ai/project/.claude/skills/create-dx-component/cookbooks/build-a-data-widget.md +281 -0
- package/src/ai/project/.claude/skills/create-dx-component/cookbooks/field-with-limits-and-validation.md +156 -0
- package/src/ai/project/.claude/skills/create-dx-component/cookbooks/status-badge-field.md +97 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/accessibility.md +71 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/choosing-type-and-subtype.md +79 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/cli-workflow.md +125 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/component-patterns.md +241 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/config-schema.md +296 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/cosmos-components.md +137 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/field-behavior.md +155 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/field-essentials.md +90 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/field-subtypes-extra.md +68 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/localization.md +30 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/pcore-pconnect-apis.md +77 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/presentation-essentials.md +57 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/props-contract.md +182 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/state-matrix.md +102 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/storybook-pattern.md +262 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/styles-pattern.md +54 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/template-essentials.md +87 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/template-patterns.md +131 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/test-pattern.md +215 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/ui-gallery-reuse.md +93 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/widget-essentials.md +84 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/widget-patterns.md +164 -0
- package/src/ai/project/.claude/skills/currency/SKILL.md +46 -0
- package/src/ai/project/.claude/skills/date/SKILL.md +46 -0
- package/src/ai/project/.claude/skills/decimal/SKILL.md +49 -0
- package/src/ai/project/.claude/skills/details/SKILL.md +60 -0
- package/src/ai/project/.claude/skills/email/SKILL.md +42 -0
- package/src/ai/project/.claude/skills/field/SKILL.md +68 -0
- package/src/ai/project/.claude/skills/form/SKILL.md +51 -0
- package/src/ai/project/.claude/skills/generate-component-set/SKILL.md +80 -0
- package/src/ai/project/.claude/skills/generate-component-set/component-manifest.json +161 -0
- package/src/ai/project/.claude/skills/identify-components/SKILL.md +76 -0
- package/src/ai/project/.claude/skills/integer/SKILL.md +45 -0
- package/src/ai/project/.claude/skills/page/SKILL.md +60 -0
- package/src/ai/project/.claude/skills/page-widget/SKILL.md +55 -0
- package/src/ai/project/.claude/skills/percentage/SKILL.md +52 -0
- package/src/ai/project/.claude/skills/phone/SKILL.md +44 -0
- package/src/ai/project/.claude/skills/picklist/SKILL.md +50 -0
- package/src/ai/project/.claude/skills/plan-dx-component/SKILL.md +51 -0
- package/src/ai/project/.claude/skills/plan-dx-component/references/design-system-mapping.md +108 -0
- package/src/ai/project/.claude/skills/plan-dx-component/references/intake.md +146 -0
- package/src/ai/project/.claude/skills/plan-dx-component/references/plan-template.md +76 -0
- package/src/ai/project/.claude/skills/plan-dx-component/references/requirements-template.md +88 -0
- package/src/ai/project/.claude/skills/presentation/SKILL.md +61 -0
- package/src/ai/project/.claude/skills/publish-library/SKILL.md +106 -0
- package/src/ai/project/.claude/skills/radiobuttons/SKILL.md +51 -0
- package/src/ai/project/.claude/skills/review-component/SKILL.md +49 -0
- package/src/ai/project/.claude/skills/review-component/references/checklist.md +77 -0
- package/src/ai/project/.claude/skills/template/SKILL.md +60 -0
- package/src/ai/project/.claude/skills/text/SKILL.md +45 -0
- package/src/ai/project/.claude/skills/update-dx-component/SKILL.md +56 -0
- package/src/ai/project/.claude/skills/update-dx-component/references/maintenance.md +43 -0
- package/src/ai/project/.claude/skills/url/SKILL.md +46 -0
- package/src/ai/project/.claude/skills/validate-component/SKILL.md +78 -0
- package/src/ai/project/.claude/skills/validate-component/references/runtime-troubleshooting.md +56 -0
- package/src/ai/project/.claude/skills/validate-component/scripts/check-component.mjs +327 -0
- package/src/ai/project/.claude/skills/widget/SKILL.md +59 -0
- package/src/ai/project/.gemini/settings.json +5 -0
- package/src/ai/project/AGENTS.md +74 -0
- package/src/ai/readme-section.md +91 -0
- package/src/cli.js +48 -7
- package/src/constants.js +54 -53
- package/src/internal-tests.js +27 -0
- package/src/tasks/ai-toggle/index.js +46 -0
- package/src/tasks/authenticate/helper.js +38 -38
- package/src/tasks/authenticate/index.js +17 -15
- package/src/tasks/build-all/helper.js +4 -4
- package/src/tasks/build-all/index.js +7 -7
- package/src/tasks/build-comp/helper.js +17 -17
- package/src/tasks/build-comp/index.js +10 -10
- package/src/tasks/build-jest-assets/index.js +7 -9
- package/src/tasks/build-lib/helper.js +8 -8
- package/src/tasks/build-lib/index.js +8 -8
- package/src/tasks/bundle/builder/index.js +9 -12
- package/src/tasks/bundle/builder/modules.js +8 -9
- package/src/tasks/bundle/builder/paths.js +2 -19
- package/src/tasks/bundle/builder/webpack.config.js +60 -77
- package/src/tasks/bundle/index.js +8 -8
- package/src/tasks/create/index.js +35 -35
- package/src/tasks/create/templates/Constellation/Field/Boolean/index.tsx.mustache +1 -36
- package/src/tasks/create/templates/Constellation/Field/Date/date.ts.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Field/Date/index.tsx.mustache +2 -27
- package/src/tasks/create/templates/Constellation/Field/DateTime/date-utils.ts.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Field/DateTime/date.ts.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Field/DateTime/index.tsx.mustache +2 -35
- package/src/tasks/create/templates/Constellation/Field/Decimal/index.tsx.mustache +3 -36
- package/src/tasks/create/templates/Constellation/Field/Decimal-Currency/index.tsx.mustache +4 -39
- package/src/tasks/create/templates/Constellation/Field/Decimal-Percentage/index.tsx.mustache +4 -53
- package/src/tasks/create/templates/Constellation/Field/Icon-Button-URL/index.tsx.mustache +5 -5
- package/src/tasks/create/templates/Constellation/Field/Integer/index.tsx.mustache +3 -36
- package/src/tasks/create/templates/Constellation/Field/Picklist/index.tsx.mustache +7 -42
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/component-constants.ts +1 -1
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/date.ts.mustache +0 -2
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/embed-utils.ts.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/index.tsx.mustache +11 -11
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/repeat-utils.ts.mustache +3 -13
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/textformat-utils.tsx.mustache +2 -3
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/utils.ts.mustache +28 -28
- package/src/tasks/create/templates/Constellation/Field/Picklist-RadioButtons/index.tsx.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Field/Picklist-RadioButtons/textformat-utils.tsx.mustache +2 -3
- package/src/tasks/create/templates/Constellation/Field/Text/index.tsx.mustache +4 -33
- package/src/tasks/create/templates/Constellation/Field/Text-Email/index.tsx.mustache +3 -33
- package/src/tasks/create/templates/Constellation/Field/Text-Input/index.tsx.mustache +3 -31
- package/src/tasks/create/templates/Constellation/Field/Text-Paragraph/index.tsx.mustache +3 -31
- package/src/tasks/create/templates/Constellation/Field/Text-Phone/index.tsx.mustache +3 -35
- package/src/tasks/create/templates/Constellation/Field/Text-URL/index.tsx.mustache +3 -33
- package/src/tasks/create/templates/Constellation/Field/Text-URL/text-url.tsx.mustache +2 -3
- package/src/tasks/create/templates/Constellation/Field/TimeofDay/index.tsx.mustache +2 -35
- package/src/tasks/create/templates/Constellation/Field/TimeofDay/time-of-day.ts.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Template/DETAILS/Avatar.tsx.mustache +19 -15
- package/src/tasks/create/templates/Constellation/Template/DETAILS/Operator.tsx.mustache +6 -6
- package/src/tasks/create/templates/Constellation/Template/DETAILS/demo.stories.tsx.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Template/DETAILS/index.tsx.mustache +4 -8
- package/src/tasks/create/templates/Constellation/Template/DETAILS/mock.ts.mustache +9 -9
- package/src/tasks/create/templates/Constellation/Template/DETAILS/utils.js.mustache +4 -8
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/Avatar.tsx.mustache +19 -15
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/Operator.tsx.mustache +6 -6
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/demo.stories.tsx.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/index.tsx.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/mock.ts.mustache +9 -9
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/utils.ts.mustache +4 -8
- package/src/tasks/create/templates/Constellation/Template/FORM/demo.stories.tsx.mustache +8 -8
- package/src/tasks/create/templates/Constellation/Template/FORM-region/demo.stories.tsx.mustache +8 -8
- package/src/tasks/create/templates/Constellation/Template/PAGE/utils.ts.mustache +4 -9
- package/src/tasks/create/templates/Constellation/Widget/CASE/demo.test.tsx.mustache +3 -3
- package/src/tasks/create/templates/Constellation/Widget/PAGE-CASE/Avatar.tsx.mustache +19 -14
- package/src/tasks/create/templates/Constellation/Widget/PAGE-CASE/Operator.tsx.mustache +6 -6
- package/src/tasks/create/templates/Constellation/Widget/PAGE-CASE/index.tsx.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Widget/PAGE-CASE/mock.ts.mustache +1 -1
- package/src/tasks/create/templates/Launchpad/Field/Icon-Button-URL/index.tsx.mustache +5 -5
- package/src/tasks/create/templates/Launchpad/Field/Text-Phone/index.tsx.mustache +3 -35
- package/src/tasks/create/templates/Launchpad/Field/__components.md +1 -1
- package/src/tasks/create/templates/Launchpad/Template/FORM/demo.stories.tsx.mustache +8 -8
- package/src/tasks/create/templates/Launchpad/Template/FORM-region/demo.stories.tsx.mustache +8 -8
- package/src/tasks/create/templates/Launchpad/Widget/CASE/demo.test.tsx.mustache +3 -3
- package/src/tasks/create/templates/Launchpad/Widget/PAGE-CASE/index.tsx.mustache +1 -1
- package/src/tasks/create/templates/Launchpad/Widget/PAGE-CASE/mock.ts.mustache +1 -1
- package/src/tasks/create-all/index.js +42 -41
- package/src/tasks/create-lib/helper.js +13 -13
- package/src/tasks/create-lib/index.js +5 -5
- package/src/tasks/create-version/helper.js +10 -10
- package/src/tasks/create-version/index.js +3 -4
- package/src/tasks/delete/helper.js +20 -19
- package/src/tasks/delete/index.js +2 -2
- package/src/tasks/delete-all/helper.js +26 -25
- package/src/tasks/delete-all/index.js +2 -2
- package/src/tasks/delete-lib/helper.js +6 -6
- package/src/tasks/delete-lib/index.js +6 -6
- package/src/tasks/delete-pub/helper.js +18 -18
- package/src/tasks/delete-pub/index.js +8 -8
- package/src/tasks/delete-version/index.js +2 -2
- package/src/tasks/dev-server/index.js +1 -1
- package/src/tasks/export-archive/helper.js +21 -21
- package/src/tasks/export-archive/index.js +27 -40
- package/src/tasks/export-rc/helper.js +34 -34
- package/src/tasks/export-rc/index.js +21 -31
- package/src/tasks/import-archive/helper.js +39 -39
- package/src/tasks/import-archive/index.js +69 -89
- package/src/tasks/import-comp/helper.js +8 -8
- package/src/tasks/import-comp/index.js +30 -42
- package/src/tasks/index.js +5 -0
- package/src/tasks/init/Node.gitignore +149 -0
- package/src/tasks/init/helper.js +15 -8
- package/src/tasks/init/index.js +115 -35
- package/src/tasks/init/template/.prettierrc.json +4 -1
- package/src/tasks/init/template/.storybook/main.ts +1 -4
- package/src/tasks/init/template/.storybook/manager.ts +1 -1
- package/src/tasks/init/template/.storybook/preview.tsx +2 -2
- package/src/tasks/init/template/.storybook/theme.js +1 -1
- package/src/tasks/init/template/.vscode/extensions.json +0 -1
- package/src/tasks/init/template/README.md +2 -2
- package/src/tasks/init/template/eslint.config.mjs +13 -155
- package/src/tasks/init/template/jest.config.cjs +2 -0
- package/src/tasks/init/template/package-lock.json +26864 -0
- package/src/tasks/init/template/package.json.mustache +19 -74
- package/src/tasks/init/template/setupTests.ts +10 -4
- package/src/tasks/init/template/tsconfig.json +14 -6
- package/src/tasks/internal-test/index.js +21 -0
- package/src/tasks/internal-test-full/index.js +15 -0
- package/src/tasks/internal-test-functional/index.js +15 -0
- package/src/tasks/internal-test-unit/index.js +13 -0
- package/src/tasks/linter/index.js +2 -2
- package/src/tasks/list/helper.js +41 -40
- package/src/tasks/list/index.js +3 -3
- package/src/tasks/list-lib/helper.js +5 -5
- package/src/tasks/list-lib/index.js +8 -8
- package/src/tasks/list-pub/helper.js +23 -16
- package/src/tasks/list-pub/index.js +24 -21
- package/src/tasks/move/helper.js +8 -8
- package/src/tasks/move/index.js +19 -23
- package/src/tasks/promote-webpack/index.js +4 -4
- package/src/tasks/promote-webpack/webpack.config.js +62 -93
- package/src/tasks/publish/helper.js +21 -21
- package/src/tasks/publish/index.js +16 -16
- package/src/tasks/publish-all/helper.js +20 -20
- package/src/tasks/publish-all/index.js +11 -12
- package/src/tasks/publish-archive/helper.js +9 -9
- package/src/tasks/publish-archive/index.js +12 -14
- package/src/tasks/publish-lib/helper.js +3 -3
- package/src/tasks/publish-lib/index.js +28 -34
- package/src/tasks/quarantine/index.js +8 -8
- package/src/tasks/rename/index.js +26 -30
- package/src/tasks/rename-lib/index.js +21 -25
- package/src/tasks/restore/index.js +8 -8
- package/src/tasks/restore-config/index.js +3 -5
- package/src/tasks/set-perm/index.js +8 -8
- package/src/tasks/show-help/index.js +17 -9
- package/src/tasks/show-status/index.js +2 -2
- package/src/tasks/store/index.js +8 -8
- package/src/tasks/switch-lib/helper.js +7 -7
- package/src/tasks/switch-lib/index.js +7 -7
- package/src/tasks/switch-version/helper.js +6 -6
- package/src/tasks/switch-version/index.js +7 -7
- package/src/tasks/update/index.js +1 -1
- package/src/tasks/upgrade/index.js +7 -7
- package/src/tasks/validator/index.js +49 -203
- package/src/util.js +169 -186
- package/src/rules/no-pcore-pconnect-in-presentation.js +0 -103
- package/src/tasks/init/template/.stylelintrc.json +0 -4
- package/src/tasks/init/template/babel.config.cjs +0 -22
- package/src/tasks/init/template/cspell.json +0 -14
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Build a data-driven widget with loading, empty and error states
|
|
3
|
+
keywords: [widget, data page, getDataApiUtils, getData, loading state, empty state, error state, retry, localization, useEffect]
|
|
4
|
+
primary_methods: [DataApiUtils.getData, PConnect.getContextName, LocalizationService.getLocalizedText]
|
|
5
|
+
related: [field-with-limits-and-validation]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Recipe: Build a data-driven widget with loading, empty and error states
|
|
9
|
+
|
|
10
|
+
> Part of the [Component Authoring cookbook](./README.md) — [skill overview](../SKILL.md).
|
|
11
|
+
|
|
12
|
+
## Problem
|
|
13
|
+
|
|
14
|
+
A page needs a panel that lists rows from a data page (for example the operator's open work). The data page and the number of rows must be configurable. The panel must show a loading indicator, an empty message when there is nothing to show, and an error with a retry button when the request fails.
|
|
15
|
+
|
|
16
|
+
## Solution
|
|
17
|
+
|
|
18
|
+
**1. Scaffold a page widget** (run `npm run createLib -- Lib 1.0.0 Y Current` first in a new project):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run create -- Widget PAGE WorkPanel "Work panel" 1.0.0 Lib "Panel listing open work" Acme
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**2. Replace `properties` in `config.json`** so a designer can choose the title, data page and row count:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
"properties": [
|
|
28
|
+
{ "name": "title", "label": "Title", "format": "TEXT", "defaultValue": "Work list" },
|
|
29
|
+
{ "name": "dataPage", "label": "Data page", "format": "TEXT", "defaultValue": "D_pyMyWorkList" },
|
|
30
|
+
{ "name": "pageSize", "label": "Rows to show", "format": "NUMBER", "defaultValue": 10 },
|
|
31
|
+
{
|
|
32
|
+
"label": "Conditions",
|
|
33
|
+
"format": "GROUP",
|
|
34
|
+
"properties": [{ "name": "visibility", "label": "Visibility", "format": "VISIBILITY" }]
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**3. Write `index.tsx`:**
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
43
|
+
import { withConfiguration, Table, EmptyState, ErrorState } from '@pega/cosmos-react-core';
|
|
44
|
+
import type { PConnFieldProps } from '../shared/PConnProps';
|
|
45
|
+
import '../shared/create-nonce';
|
|
46
|
+
|
|
47
|
+
import StyledAcmeLibWorkPanelWrapper from './styles';
|
|
48
|
+
|
|
49
|
+
interface AcmeLibWorkPanelProps extends PConnFieldProps {
|
|
50
|
+
title?: string;
|
|
51
|
+
dataPage?: string;
|
|
52
|
+
pageSize?: number | string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface WorkRow {
|
|
56
|
+
id: number;
|
|
57
|
+
caseType: string;
|
|
58
|
+
status: string;
|
|
59
|
+
stage: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type LoadState = 'loading' | 'ready' | 'error';
|
|
63
|
+
|
|
64
|
+
function AcmeLibWorkPanel(props: AcmeLibWorkPanelProps) {
|
|
65
|
+
const { getPConnect, title = 'Work list', dataPage = 'D_pyMyWorkList', pageSize = 10 } = props;
|
|
66
|
+
const pConn = getPConnect();
|
|
67
|
+
const context = pConn.getContextName();
|
|
68
|
+
const localize = (text: string) => pConn.getLocalizationService().getLocalizedText(text);
|
|
69
|
+
|
|
70
|
+
const [rows, setRows] = useState<WorkRow[]>([]);
|
|
71
|
+
const [state, setState] = useState<LoadState>('loading');
|
|
72
|
+
const [attempt, setAttempt] = useState(0);
|
|
73
|
+
const size = Number.parseInt(String(pageSize), 10) || 10;
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
setState('loading');
|
|
78
|
+
|
|
79
|
+
PCore.getDataApiUtils()
|
|
80
|
+
.getData(dataPage, { paging: { pageNumber: 1, pageSize: size } }, context)
|
|
81
|
+
.then((response: any) => {
|
|
82
|
+
if (cancelled) return;
|
|
83
|
+
const data: any[] = response?.data?.data ?? [];
|
|
84
|
+
setRows(
|
|
85
|
+
data.map((entry, index) => ({
|
|
86
|
+
id: index,
|
|
87
|
+
caseType: entry.pxProcessName,
|
|
88
|
+
status: entry.pyAssignmentStatus,
|
|
89
|
+
stage: entry.pxTaskLabel
|
|
90
|
+
}))
|
|
91
|
+
);
|
|
92
|
+
setState('ready');
|
|
93
|
+
})
|
|
94
|
+
.catch(() => {
|
|
95
|
+
if (cancelled) return;
|
|
96
|
+
setRows([]);
|
|
97
|
+
setState('error');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ignore a response that arrives after the component unmounted or the inputs changed
|
|
101
|
+
return () => {
|
|
102
|
+
cancelled = true;
|
|
103
|
+
};
|
|
104
|
+
}, [context, dataPage, size, attempt]);
|
|
105
|
+
|
|
106
|
+
const retry = useCallback(() => setAttempt((count) => count + 1), []);
|
|
107
|
+
|
|
108
|
+
if (state === 'error') {
|
|
109
|
+
return (
|
|
110
|
+
<StyledAcmeLibWorkPanelWrapper>
|
|
111
|
+
<ErrorState message={localize('Unable to load work')} onRetry={retry} />
|
|
112
|
+
</StyledAcmeLibWorkPanelWrapper>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (state === 'ready' && rows.length === 0) {
|
|
117
|
+
return (
|
|
118
|
+
<StyledAcmeLibWorkPanelWrapper>
|
|
119
|
+
<EmptyState message={localize('No work found')} />
|
|
120
|
+
</StyledAcmeLibWorkPanelWrapper>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const columns: { renderer: keyof WorkRow; label: string }[] = [
|
|
125
|
+
{ renderer: 'caseType', label: localize('Case type') },
|
|
126
|
+
{ renderer: 'status', label: localize('Status') },
|
|
127
|
+
{ renderer: 'stage', label: localize('Stage') }
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<StyledAcmeLibWorkPanelWrapper>
|
|
132
|
+
<Table
|
|
133
|
+
title={localize(title)}
|
|
134
|
+
columns={columns}
|
|
135
|
+
data={rows}
|
|
136
|
+
loading={state === 'loading'}
|
|
137
|
+
loadingMessage={localize('Loading work')}
|
|
138
|
+
/>
|
|
139
|
+
</StyledAcmeLibWorkPanelWrapper>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default withConfiguration(AcmeLibWorkPanel);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**4. Add every user-visible string to `localizations.json`** under `fields`:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"fields": {
|
|
151
|
+
"Case type": "Case type",
|
|
152
|
+
"Status": "Status",
|
|
153
|
+
"Stage": "Stage",
|
|
154
|
+
"Work list": "Work list",
|
|
155
|
+
"Loading work": "Loading work",
|
|
156
|
+
"No work found": "No work found",
|
|
157
|
+
"Unable to load work": "Unable to load work"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**5. Replace `mock.ts`, `demo.stories.tsx` and `demo.test.tsx`.** The story fakes only `PCore.getDataApiUtils().getData` and the two `pConn` methods the widget calls. Three stories drive the three states:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
// mock.ts
|
|
166
|
+
export const configProps = { title: 'Open work', dataPage: 'D_pyMyWorkList', pageSize: 10 };
|
|
167
|
+
|
|
168
|
+
export const workListData = {
|
|
169
|
+
data: {
|
|
170
|
+
data: [
|
|
171
|
+
{ pxProcessName: 'Loan request', pyAssignmentStatus: 'Open', pxTaskLabel: 'Review' },
|
|
172
|
+
{ pxProcessName: 'Service request', pyAssignmentStatus: 'Pending', pxTaskLabel: 'Approve' }
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
// demo.stories.tsx
|
|
180
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
181
|
+
|
|
182
|
+
import AcmeLibWorkPanel from './index';
|
|
183
|
+
import { configProps, workListData } from './mock';
|
|
184
|
+
|
|
185
|
+
const meta: Meta<typeof AcmeLibWorkPanel> = {
|
|
186
|
+
title: 'AcmeLibWorkPanel',
|
|
187
|
+
component: AcmeLibWorkPanel,
|
|
188
|
+
excludeStories: /.*Data$/
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default meta;
|
|
192
|
+
type Story = StoryObj<typeof AcmeLibWorkPanel>;
|
|
193
|
+
|
|
194
|
+
// Fake only the PCore function the component calls.
|
|
195
|
+
const mockGetData = (getData: () => Promise<unknown>) => {
|
|
196
|
+
(globalThis as unknown as { PCore: unknown }).PCore = { getDataApiUtils: () => ({ getData }) };
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const getPConnect = () =>
|
|
200
|
+
({
|
|
201
|
+
getContextName: () => 'app/primary_1',
|
|
202
|
+
getLocalizationService: () => ({ getLocalizedText: (text: string) => text })
|
|
203
|
+
}) as any;
|
|
204
|
+
|
|
205
|
+
const renderPanel = (getData: () => Promise<unknown>) => {
|
|
206
|
+
const Render: Story = (args: any) => {
|
|
207
|
+
mockGetData(getData);
|
|
208
|
+
return <AcmeLibWorkPanel getPConnect={getPConnect} {...args} />;
|
|
209
|
+
};
|
|
210
|
+
Render.args = { ...configProps };
|
|
211
|
+
return Render;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const BaseAcmeLibWorkPanel = renderPanel(() => Promise.resolve(workListData));
|
|
215
|
+
export const EmptyAcmeLibWorkPanel = renderPanel(() => Promise.resolve({ data: { data: null } }));
|
|
216
|
+
export const ErrorAcmeLibWorkPanel = renderPanel(() => Promise.reject(new Error('Network error')));
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// demo.test.tsx
|
|
221
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
222
|
+
import { composeStories } from '@storybook/react';
|
|
223
|
+
import '@testing-library/jest-dom';
|
|
224
|
+
|
|
225
|
+
import * as DemoStories from './demo.stories';
|
|
226
|
+
import { workListData } from './mock';
|
|
227
|
+
|
|
228
|
+
const { BaseAcmeLibWorkPanel, EmptyAcmeLibWorkPanel, ErrorAcmeLibWorkPanel } = composeStories(DemoStories);
|
|
229
|
+
|
|
230
|
+
test('shows the loading message, then the rows', async () => {
|
|
231
|
+
render(<BaseAcmeLibWorkPanel />);
|
|
232
|
+
expect(screen.getByText('Loading work')).toBeVisible();
|
|
233
|
+
|
|
234
|
+
expect(await screen.findByText('Loan request')).toBeVisible();
|
|
235
|
+
expect(screen.getByText('Service request')).toBeVisible();
|
|
236
|
+
expect(screen.getByText('Case type')).toBeVisible();
|
|
237
|
+
expect(screen.queryByText('Loading work')).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('shows the empty state when the data page returns no rows', async () => {
|
|
241
|
+
render(<EmptyAcmeLibWorkPanel />);
|
|
242
|
+
expect(await screen.findByText('No work found')).toBeVisible();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('shows an error and recovers when the user retries', async () => {
|
|
246
|
+
render(<ErrorAcmeLibWorkPanel />);
|
|
247
|
+
expect(await screen.findByText('Unable to load work')).toBeVisible();
|
|
248
|
+
|
|
249
|
+
// the next request succeeds
|
|
250
|
+
(globalThis as unknown as { PCore: { getDataApiUtils: () => unknown } }).PCore.getDataApiUtils = () => ({
|
|
251
|
+
getData: () => Promise.resolve(workListData)
|
|
252
|
+
});
|
|
253
|
+
fireEvent.click(screen.getByRole('button', { name: /try again/i }));
|
|
254
|
+
|
|
255
|
+
expect(await screen.findByText('Loan request')).toBeVisible();
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**6. Run the check loop** from [cli-workflow.md](../references/cli-workflow.md):
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npx jest src/components/Acme_Lib_WorkPanel --coverage=false
|
|
263
|
+
npx tsc --noEmit 2>&1 | grep -v "demo.stories.tsx"
|
|
264
|
+
npm run validate-schema -- Acme_Lib_WorkPanel
|
|
265
|
+
npm run buildComponent -- Acme_Lib_WorkPanel N N
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Discussion
|
|
269
|
+
|
|
270
|
+
- **MUST** handle all four outcomes of the request: pending, rows, no rows (`response.data.data` is `null` or empty) and failure. A widget that handles only the success path shows an empty table forever when the call fails.
|
|
271
|
+
- **MUST** guard against a stale response. The `cancelled` flag in the effect cleanup stops a late response from updating state after the component unmounts or `dataPage`/`context` change.
|
|
272
|
+
- Keep the effect dependencies to the inputs that should trigger a reload (`context`, `dataPage`, the page size and a retry counter). Putting `pConn` or a freshly created object in the dependency list reloads on every render. See [performance-and-efficiency.md](../../best-practices/performance-and-efficiency.md).
|
|
273
|
+
- `getData(dataViewName, payload?, context?, options?)` is documented in [pcore-rest-data-details.md](../../references/pcore-rest-data-details.md). `getContextName()` is in [pconnect-reference-context-component.md](../../references/pconnect-reference-context-component.md) and `getLocalizedText` in [pconnect-case-info-data-info-localization.md](../../references/pconnect-case-info-data-info-localization.md).
|
|
274
|
+
- `ErrorState` renders its retry button with the design system's own text ("Try again"). Query it by role and name in tests.
|
|
275
|
+
- In the story, assign `PCore` through `globalThis` instead of `window.PCore = ...`. The `globals.d.ts` in the project declares `PCore` as a global constant, so the scaffolded `window.PCore` assignments produce `tsc` errors.
|
|
276
|
+
- If the widget must call a REST endpoint instead of a data page, see [pcore-rest-data-details.md](../../references/pcore-rest-data-details.md) and the [error handling guide](../../best-practices/error-handling-and-resiliency.md).
|
|
277
|
+
|
|
278
|
+
## Related recipes
|
|
279
|
+
|
|
280
|
+
- [Customize a field with limits, a counter and validation messages](./field-with-limits-and-validation.md)
|
|
281
|
+
- [Subscribe to data page changes](../../pconnect-data-subscriptions/SKILL.md)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Customize a field with limits, a counter and validation messages
|
|
3
|
+
keywords: [field, text input, updateFieldValue, triggerFieldChange, validatemessage, max length, uppercase, config.json property]
|
|
4
|
+
primary_methods: [ActionsApi.updateFieldValue, ActionsApi.triggerFieldChange, PConnect.getStateProps]
|
|
5
|
+
related: [build-a-data-widget]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Recipe: Customize a field with limits, a counter and validation messages
|
|
9
|
+
|
|
10
|
+
> Part of the [Component Authoring cookbook](./README.md) — [skill overview](../SKILL.md).
|
|
11
|
+
|
|
12
|
+
## Problem
|
|
13
|
+
|
|
14
|
+
A text field must limit how many characters can be typed, optionally convert input to uppercase, show a `n/limit` counter, and show the platform's validation message. The limit and the uppercase option must be configurable by the designer.
|
|
15
|
+
|
|
16
|
+
## Solution
|
|
17
|
+
|
|
18
|
+
Run every command from the project root. Replace `Acme` and `Lib` with the project's organization and library.
|
|
19
|
+
|
|
20
|
+
**1. Scaffold the field** (library mode needs `npm run createLib -- Lib 1.0.0 Y Current` once first; see [cli-workflow.md](../references/cli-workflow.md)):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run create -- Field Text CountedText "Counted text" 1.0.0 Lib "Text field with a character counter" Acme
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**2. Add the designer-facing properties** to `src/components/Acme_Lib_CountedText/config.json`, next to `placeholder`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{ "name": "maxLength", "label": "Maximum characters", "format": "NUMBER", "defaultValue": 50 },
|
|
30
|
+
{ "name": "forceUppercase", "label": "Convert to uppercase", "format": "BOOLEAN", "defaultValue": false }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**3. Read the properties in `index.tsx`.** Declare them in the props interface, give them defaults, and normalize them because they can arrive as strings:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
interface AcmeLibCountedTextProps extends PConnFieldProps {
|
|
37
|
+
// ...scaffolded props (isTableFormatter, formatter, variant)
|
|
38
|
+
maxLength?: number | string;
|
|
39
|
+
forceUppercase?: boolean | string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { /* ...scaffolded props */ validatemessage, maxLength = 50, forceUppercase = false } = props;
|
|
43
|
+
|
|
44
|
+
const limit = Number.parseInt(String(maxLength), 10) || 50;
|
|
45
|
+
const upper = forceUppercase === true || forceUppercase === 'true';
|
|
46
|
+
const normalize = (raw: string) => (upper ? raw.toUpperCase() : raw).slice(0, limit);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**4. Write the value through the actions API.** `updateFieldValue` on every change, `triggerFieldChange` on blur:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
const handleOnChange = (event: any) => {
|
|
53
|
+
actions.updateFieldValue(propName, normalize(event.target.value));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleOnBlur = (event: any) => {
|
|
57
|
+
actions.triggerFieldChange(propName, normalize(event.target.value));
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`propName` is `pConn.getStateProps().value` (for example `.TextSample`), as in the scaffold. Leave the scaffolded `DISPLAY_ONLY` branch untouched so the field still works in details views and tables.
|
|
62
|
+
|
|
63
|
+
**5. Render the counter and the validation message** on the editable `Input`:
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<Input
|
|
67
|
+
type='text'
|
|
68
|
+
value={value}
|
|
69
|
+
label={label}
|
|
70
|
+
labelHidden={hideLabel}
|
|
71
|
+
placeholder={placeholder}
|
|
72
|
+
disabled={disabled}
|
|
73
|
+
readOnly={readOnly}
|
|
74
|
+
required={required}
|
|
75
|
+
onChange={handleOnChange}
|
|
76
|
+
onBlur={handleOnBlur}
|
|
77
|
+
status={validatemessage ? 'error' : undefined}
|
|
78
|
+
info={validatemessage || `${(value ?? '').length}/${limit}`}
|
|
79
|
+
testId={testId}
|
|
80
|
+
/>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**6. Extend the story and tests.** In `demo.stories.tsx`, forward the blur call to a story-only arg so a test can observe it:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
triggerFieldChange: (propName: string, theValue: any) => {
|
|
87
|
+
args.onTriggerFieldChange?.(propName, theValue);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Replace the scaffolded test in `demo.test.tsx`:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
95
|
+
import { composeStories } from '@storybook/react';
|
|
96
|
+
import '@testing-library/jest-dom';
|
|
97
|
+
|
|
98
|
+
import * as DemoStories from './demo.stories';
|
|
99
|
+
|
|
100
|
+
const { BaseAcmeLibCountedText } = composeStories(DemoStories);
|
|
101
|
+
|
|
102
|
+
const getInput = () => screen.getByTestId('Text-12345678:input:control') as HTMLInputElement;
|
|
103
|
+
|
|
104
|
+
test('shows the character counter', async () => {
|
|
105
|
+
render(<BaseAcmeLibCountedText maxLength={10} />);
|
|
106
|
+
expect(await screen.findByText('0/10')).toBeVisible();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('truncates to the limit, converts to uppercase and commits on blur', async () => {
|
|
110
|
+
const onTriggerFieldChange = jest.fn();
|
|
111
|
+
// story-only arg: read by the story's fake getActionsApi, not a prop of the component
|
|
112
|
+
const storyArgs: Record<string, unknown> = { onTriggerFieldChange };
|
|
113
|
+
render(<BaseAcmeLibCountedText maxLength={5} forceUppercase {...storyArgs} />);
|
|
114
|
+
|
|
115
|
+
fireEvent.change(getInput(), { target: { value: 'hello world' } });
|
|
116
|
+
expect(getInput().value).toBe('HELLO');
|
|
117
|
+
expect(await screen.findByText('5/5')).toBeVisible();
|
|
118
|
+
|
|
119
|
+
fireEvent.blur(getInput());
|
|
120
|
+
expect(onTriggerFieldChange).toHaveBeenCalledWith('.TextSample', 'HELLO');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('shows the validation message instead of the counter', async () => {
|
|
124
|
+
render(<BaseAcmeLibCountedText validatemessage='Value is required' />);
|
|
125
|
+
expect(await screen.findByText('Value is required')).toBeVisible();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('renders only the value in DISPLAY_ONLY mode', async () => {
|
|
129
|
+
render(<BaseAcmeLibCountedText displayMode='DISPLAY_ONLY' value='Shown' />);
|
|
130
|
+
expect(await screen.findByText('Shown')).toBeVisible();
|
|
131
|
+
expect(screen.queryByTestId('Text-12345678:input:control')).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**7. Run the check loop** from [cli-workflow.md](../references/cli-workflow.md):
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx jest src/components/Acme_Lib_CountedText --coverage=false
|
|
139
|
+
npx tsc --noEmit 2>&1 | grep -v "demo.stories.tsx"
|
|
140
|
+
npm run validate-schema -- Acme_Lib_CountedText
|
|
141
|
+
npm run buildComponent -- Acme_Lib_CountedText N N
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Discussion
|
|
145
|
+
|
|
146
|
+
- **MUST** keep the two-step write: `updateFieldValue` while typing so the store and the screen stay in sync, and `triggerFieldChange` on blur so validation and dependent fields run once the user is done. Calling only `triggerFieldChange` on every keystroke causes extra server round trips; calling only `updateFieldValue` never commits the change. Signatures: [pconnect-actions-api.md](../../references/pconnect-actions-api.md).
|
|
147
|
+
- **MUST** normalize the value the same way in both handlers, otherwise the committed value can differ from what the user sees.
|
|
148
|
+
- Declare a property in `config.json` **and** in the props interface, and give it a default. A designer can leave it empty and `maxLength` would otherwise be `undefined`.
|
|
149
|
+
- Keep the read-only display branches. A field that only renders an input looks broken in details views ([props-contract.md](../references/props-contract.md)).
|
|
150
|
+
- Use `fireEvent` from `@testing-library/react` in tests; the template does not install `@testing-library/user-event`.
|
|
151
|
+
- Passing `onTriggerFieldChange` through a `Record<string, unknown>` spread keeps `tsc` clean without adding a test-only prop to the component.
|
|
152
|
+
|
|
153
|
+
## Related recipes
|
|
154
|
+
|
|
155
|
+
- [Build a data-driven widget with loading, empty and error states](./build-a-data-widget.md)
|
|
156
|
+
- [Form and field validation APIs](../../pconnect-forms-and-fields/SKILL.md)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Show a property as a colored status badge (display-only Field)
|
|
3
|
+
keywords: [status, badge, pill, display-only, DISPLAY_ONLY, Status, variant, Field, Text, statusMap, read-only value]
|
|
4
|
+
primary_methods: [getStateProps, Status]
|
|
5
|
+
related: [field-with-limits-and-validation, build-a-data-widget]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Recipe: Show a property as a colored status badge
|
|
9
|
+
|
|
10
|
+
> Part of the [Component Authoring cookbook](./README.md) — [skill overview](../SKILL.md).
|
|
11
|
+
|
|
12
|
+
## Problem
|
|
13
|
+
|
|
14
|
+
A case property holds a short status such as `Approved` or `Rejected`. In a details view or a table the user wants to see it as a colored badge, and the app designer should choose which value gets which color in App Studio. The same Field should still work as a normal text input when the view is editable.
|
|
15
|
+
|
|
16
|
+
## Solution
|
|
17
|
+
|
|
18
|
+
A read-only badge bound to a property is a **Field** (subtype `Text`) that handles `DISPLAY_ONLY`; it is not a Presentation component, and it is a Widget only if it loads its own data. See [choosing-type-and-subtype.md](../references/choosing-type-and-subtype.md).
|
|
19
|
+
|
|
20
|
+
**1. Scaffold a Text field:**
|
|
21
|
+
|
|
22
|
+
`npm run create -- Field Text StatusBadge "Status badge" 1.0.0 Lib "Shows a status as a colored badge" Acme`
|
|
23
|
+
|
|
24
|
+
**2. Add one authoring property to `config.json`**, after the existing ones. The designer types value and color pairs:
|
|
25
|
+
|
|
26
|
+
| Property | Label | Format | Notes |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `statusMap` | Status colors | `TEXT` | helper text: "Value=color pairs, comma separated. Colors: success, urgent, warn, pending, info." |
|
|
29
|
+
|
|
30
|
+
**3. Edit `index.tsx`.** Import `Status` from `@pega/cosmos-react-core` and add `statusMap?: string` to the props interface. Add two small, exported pure functions above the component:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
type StatusVariant = 'success' | 'urgent' | 'warn' | 'pending' | 'info';
|
|
34
|
+
const STATUS_VARIANTS: StatusVariant[] = ['success', 'urgent', 'warn', 'pending', 'info'];
|
|
35
|
+
|
|
36
|
+
// "Approved=success, Rejected=urgent" -> { approved: 'success', rejected: 'urgent' }
|
|
37
|
+
export const parseStatusMap = (text = ''): Record<string, StatusVariant> =>
|
|
38
|
+
text.split(',').reduce<Record<string, StatusVariant>>((map, pair) => {
|
|
39
|
+
const [key, variant] = pair.split('=').map(part => part.trim());
|
|
40
|
+
if (key && STATUS_VARIANTS.includes(variant as StatusVariant)) map[key.toLowerCase()] = variant as StatusVariant;
|
|
41
|
+
return map;
|
|
42
|
+
}, {});
|
|
43
|
+
|
|
44
|
+
// An unmapped or misspelled value gets the neutral 'info' variant instead of failing.
|
|
45
|
+
export const variantFor = (value: string, statusMap?: string): StatusVariant =>
|
|
46
|
+
parseStatusMap(statusMap)[String(value).trim().toLowerCase()] ?? 'info';
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then replace only the `DISPLAY_ONLY` branch's value rendering, keeping the scaffold's empty placeholder:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
let displayComp: any = value ? (
|
|
53
|
+
<Status variant={variantFor(value, statusMap)}>{value}</Status>
|
|
54
|
+
) : (
|
|
55
|
+
<span aria-hidden='true'>––</span>
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Leave the editable `Input` as the scaffold has it. Remove the unused `variant?: any` from the props interface, because the dead-code rule and ESLint reject it.
|
|
60
|
+
|
|
61
|
+
**4. Add tests** to `demo.test.tsx`. The pure functions are the part worth testing precisely:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
test('maps values to variants, ignoring case and bad entries', () => {
|
|
65
|
+
const map = 'Approved=success, Rejected = urgent, Bad=purple, =warn, Pending=pending';
|
|
66
|
+
expect(variantFor('approved', map)).toBe('success');
|
|
67
|
+
expect(variantFor(' Rejected ', map)).toBe('urgent');
|
|
68
|
+
expect(variantFor('Pending', map)).toBe('pending');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('falls back to info for an unmapped value, a bad variant or no map', () => {
|
|
72
|
+
expect(variantFor('Unknown', 'Approved=success')).toBe('info');
|
|
73
|
+
expect(variantFor('Bad', 'Bad=purple')).toBe('info');
|
|
74
|
+
expect(variantFor('Approved')).toBe('info');
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Add a test that renders the component with `displayMode: 'DISPLAY_ONLY'` and a mapped value and checks the text is shown, and another with an empty value that checks the placeholder dash. Render the component directly with a small `getPConnect` that returns `getStateProps` and `getActionsApi`; see [test-pattern.md](../references/test-pattern.md).
|
|
79
|
+
|
|
80
|
+
**5. Add a display-only story** in `demo.stories.tsx` (value `Approved`, `statusMap` set, `displayMode: 'DISPLAY_ONLY'`) so the badge can be reviewed in Storybook. The check script warns when there is only one story.
|
|
81
|
+
|
|
82
|
+
**6. Run** `node .claude/skills/validate-component/scripts/check-component.mjs Acme_Lib_StatusBadge`.
|
|
83
|
+
|
|
84
|
+
This recipe was built in a generated project: the schema check, `tsc`, ESLint and Jest all passed (the build step was not run there).
|
|
85
|
+
|
|
86
|
+
## Discussion
|
|
87
|
+
|
|
88
|
+
- **Why a map property and not hard-coded colors.** Statuses differ between applications. Putting the pairs in `config.json` lets the designer change them without a code change, and an unmapped value still renders, as the neutral `info` badge.
|
|
89
|
+
- **`Status` takes five variants only**: `success`, `urgent`, `warn`, `pending` and `info`. The label is a string or number, and Cosmos transforms it to uppercase. Check the installed `Status.d.ts` before using another prop.
|
|
90
|
+
- **Color is never the only signal.** The badge text carries the status, so a user who cannot distinguish the colors still reads it.
|
|
91
|
+
- **Localization.** The badge shows the stored value as it is. If the values need translating, pass the text through `getLocalizedText` and add each possible value to `localizations.json`; see [localization.md](../references/localization.md).
|
|
92
|
+
- **Editable mode** is the unchanged scaffold. If users must also edit the status, make it a picklist instead (see the `picklist` skill) so the value cannot be mistyped.
|
|
93
|
+
|
|
94
|
+
## Related recipes
|
|
95
|
+
|
|
96
|
+
- [Customize a field with limits, a counter and validation messages](./field-with-limits-and-validation.md)
|
|
97
|
+
- [Build a data-driven widget](./build-a-data-widget.md)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Accessibility
|
|
2
|
+
|
|
3
|
+
Accessibility is part of the component contract, not a final polish step. Cosmos components already carry most of the semantics (labels, roles, ARIA state, focus rings, keyboard behavior), which is the main reason to build from them (see the Cosmos-first rule in [AGENTS.md](../../../../AGENTS.md)). This file lists what the component author is still responsible for, and how to verify it with the tools the project already has.
|
|
4
|
+
|
|
5
|
+
Target: WCAG 2.2 AA.
|
|
6
|
+
|
|
7
|
+
## What Cosmos gives you, and what it does not
|
|
8
|
+
|
|
9
|
+
| Cosmos provides | You must still provide |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| Label association, `required`, `aria-invalid` and error text on its inputs | A meaningful `label`, kept even when `hideLabel` is set (hide it visually, do not remove it) |
|
|
12
|
+
| Roles, `aria-selected`, roving focus and arrow keys for `Tabs`; `inert` on inactive panels | Unique, stable tab ids; a sensible active tab when one disappears |
|
|
13
|
+
| `role='alert'` on urgent and warning `Banner`, polite for the rest | Choosing the right variant, and an array for `messages` |
|
|
14
|
+
| `fieldset` and `legend` for a named `FieldGroup`, `aria-expanded` on collapsible groups | Heading levels that stay sequential (see below) |
|
|
15
|
+
| Visible `:focus-visible` rings | Not removing them in `styles.ts`, and deciding where focus goes after an action |
|
|
16
|
+
| A busy indicator in `Progress` | A status message screen readers can hear: the `Progress` in the build checked does not announce by itself |
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
1. **Semantic first.** Use the native element or the Cosmos component, then ARIA only to fill a real gap. Do not put `role` on something a Cosmos component already renders with that role.
|
|
21
|
+
2. **Every control has an accessible name** that is stable and specific. Icon-only buttons need a `label` or `aria-label`. When several controls repeat (a row of Delete buttons), give each a distinct name (the item name in the label, or `contextualLabel` on a `SummaryList` item).
|
|
22
|
+
3. **Associate help and errors with the control.** Pass `helperText` and `validatemessage` to the Cosmos control (as `info`) instead of rendering a separate paragraph. A validation message replaces helper text.
|
|
23
|
+
4. **Never convey state by color alone.** Required, error, warning and success need text or an icon as well. Cosmos `status` does this for inputs.
|
|
24
|
+
5. **Keyboard parity.** Everything a pointer can do, the keyboard can do, in a logical tab order. Hover-only information (a tooltip, a preview) needs a focus or key equivalent. Do not trap focus, except inside a modal, which Cosmos handles.
|
|
25
|
+
6. **Announce asynchronous change without moving focus.** Loading finished, save succeeded, validation failed and a list changed should be announced through a polite live region or a `Banner`. Do not steal focus for a status message. Use `aria-busy` on a region that is refreshing in place.
|
|
26
|
+
7. **Move focus deliberately, and only when the user acted.**
|
|
27
|
+
- After a destructive action on a list item, focus the next item or the Add button (`SummaryList` / `ViewAll` do this when given `deletedItemId` and `fallbackFocusRef`).
|
|
28
|
+
- After a refresh replaces the control that was activated, restore focus to a control that still exists (save `document.activeElement` first).
|
|
29
|
+
- After an error the user must fix, focus the first invalid control or the error summary (`Banner` accepts a `handle` with `focus()`).
|
|
30
|
+
- Do not move focus on load, on a timer, or when a background request finishes.
|
|
31
|
+
8. **Headings.** The page template owns the `h1`. A top-level section is `h2`, nested sections `h3` and below, with no skipped levels. Use `Text variant` or `headingTag`, not a styled `span`. A `FieldGroup` title is a `legend`, not a heading, unless you pass `headingTag`.
|
|
32
|
+
9. **Do not add landmarks** (`main`, `nav`) in a field, widget or template. The shell owns them.
|
|
33
|
+
10. **Hidden means gone.** Remove hidden content (`display: none`, `inert`, or not rendering it), do not just move it off-screen or make it transparent, so it leaves the tab order and the reading order.
|
|
34
|
+
11. **Reflow and zoom.** Content must work at 320 CSS pixels wide and at 200% zoom without two-dimensional scrolling. Use `minmax(0, 1fr)` grid tracks, wrapping text, and container-based breakpoints. Do not use fixed pixel widths for text containers.
|
|
35
|
+
12. **Motion.** Respect `prefers-reduced-motion` for any animation you add in `styles.ts`. Cosmos handles its own.
|
|
36
|
+
13. **Target size.** Interactive targets are at least 24 by 24 CSS pixels. Use Cosmos `Button` and `Link` rather than a bare clickable `span`.
|
|
37
|
+
14. **Localization.** Labels, status text and error messages go through the localization service, and the layout must tolerate text that is 30 to 40 percent longer.
|
|
38
|
+
15. **Reading order equals visual order.** In a multi-column template, the DOM order must match the order a sighted user reads; when columns stack, the semantic order must survive.
|
|
39
|
+
|
|
40
|
+
## By component type
|
|
41
|
+
|
|
42
|
+
- **Field:** the label, `required`, `disabled`, `readOnly` and error state must each be observable by role and name. A radio group has a group name. A masked or formatted input exposes the formatted value, not the raw one. The display-only form still needs a label (visible or associated). Clearing an error on edit must not silently remove the information the user needs.
|
|
43
|
+
- **Widget:** a distinct, announced state for loading, empty, error, success and pending action. A list is a list (`ul`/`ol` or a Cosmos list component), a data table is a `Table` with headers. A chart needs a text alternative or a table with the same data. A control that changes the content below it says so.
|
|
44
|
+
- **Template:** one `h1` only in a page template, sequential headings, `fieldset`/`legend` for groups of fields, `EmptyState` for an empty region, and a collapse order that keeps the reading order.
|
|
45
|
+
|
|
46
|
+
## Verification
|
|
47
|
+
|
|
48
|
+
Do these in the Jest test and in Storybook; an automated pass never replaces a keyboard check.
|
|
49
|
+
|
|
50
|
+
**In `demo.test.tsx`** (only `fireEvent` and Testing Library are installed; do not rely on `user-event`):
|
|
51
|
+
|
|
52
|
+
- Find controls by role and accessible name: `screen.getByRole('textbox', { name: /email/i })`, `getByRole('group', { name: 'Contact' })`, `getByRole('alert')`. A query that needs a test id because there is no role or name is an accessibility finding.
|
|
53
|
+
- Assert state attributes: `toBeRequired()`, `toBeDisabled()`, `toHaveAttribute('aria-invalid', 'true')`, `toHaveAttribute('aria-busy', 'true')`, `toBeVisible()`.
|
|
54
|
+
- Assert the error is exposed: after setting `validatemessage`, the message text is in the document and the control is described by it (`toHaveAccessibleDescription` where the installed jest-dom has it).
|
|
55
|
+
- Keyboard path: `control.focus()`, `expect(control).toHaveFocus()`, `fireEvent.keyDown(control, { key: 'Enter' })` or `'ArrowDown'`, and assert the result. Test the Escape and Enter paths of any popover or modal you add.
|
|
56
|
+
- Focus after an action: delete an item, then `expect(addButton).toHaveFocus()`; close a modal, then the trigger has focus.
|
|
57
|
+
- Status announcement: `expect(await screen.findByRole('status')).toHaveTextContent(...)` or the `Banner` text, for loading finished, saved and failed.
|
|
58
|
+
|
|
59
|
+
**Automated rule checking** with `jest-axe` is optional and is **not installed** in the project template. Adding it is a new dependency, so ask the user first (AGENTS.md, Third-Party Library Consent) with the library name (`jest-axe`), why (it catches missing names, invalid ARIA and contrast-independent rule violations that role queries miss), and that it would be a new dev dependency. Do not add it without approval.
|
|
60
|
+
|
|
61
|
+
**By hand, in Storybook** (`npm run startStorybook` is user-run):
|
|
62
|
+
|
|
63
|
+
1. Tab through the component without a mouse: order, visible focus, no trap, every action reachable.
|
|
64
|
+
2. Operate every action with Enter and Space, and every list or tab group with the arrow keys.
|
|
65
|
+
3. Zoom to 200% and narrow the viewport to 320px; nothing is clipped or needs horizontal scrolling.
|
|
66
|
+
4. Turn on a screen reader (VoiceOver on macOS) and check the name, role, state and announcement for the default, error, loading and empty states.
|
|
67
|
+
5. Check the error, required and read-only states without relying on color.
|
|
68
|
+
|
|
69
|
+
## Reporting
|
|
70
|
+
|
|
71
|
+
In the final report, say which of these were done and which were not. Never claim an accessibility pass from the automated tests alone.
|