@salesforce/templates 66.6.2 → 66.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/lib/generators/projectGenerator.js +4 -4
  2. package/lib/generators/projectGenerator.js.map +1 -1
  3. package/lib/generators/{webApplicationGenerator.d.ts → uiBundleGenerator.d.ts} +2 -2
  4. package/lib/generators/{webApplicationGenerator.js → uiBundleGenerator.js} +23 -22
  5. package/lib/generators/uiBundleGenerator.js.map +1 -0
  6. package/lib/i18n/i18n.d.ts +1 -1
  7. package/lib/i18n/i18n.js +1 -1
  8. package/lib/i18n/i18n.js.map +1 -1
  9. package/lib/templates/project/reactexternalapp/AGENT.md +152 -46
  10. package/lib/templates/project/reactexternalapp/CHANGELOG.md +366 -208
  11. package/lib/templates/project/reactexternalapp/README.md +16 -16
  12. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/CHANGELOG.md +1 -1
  13. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/README.md +1 -1
  14. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/eslint.config.js +13 -2
  15. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package.json +3 -3
  16. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/reactexternalapp.uibundle-meta.xml +8 -0
  17. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/scripts/get-graphql-schema.mjs +1 -1
  18. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +15 -6
  19. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/FilterContext.tsx +13 -3
  20. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/BooleanFilter.tsx +9 -5
  21. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/DateFilter.tsx +15 -8
  22. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/DateRangeFilter.tsx +8 -7
  23. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/FilterFieldWrapper.tsx +33 -0
  24. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/MultiSelectFilter.tsx +4 -5
  25. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +118 -40
  26. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +24 -11
  27. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SelectFilter.tsx +9 -5
  28. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +29 -12
  29. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useDebouncedCallback.ts +34 -0
  30. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useObjectSearchParams.ts +10 -5
  31. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/debounce.ts +4 -1
  32. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/filterUtils.ts +24 -1
  33. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/context/AuthContext.tsx +2 -2
  34. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/hooks/useCountdownTimer.ts +1 -1
  35. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Profile.tsx +3 -3
  36. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/pages/Register.tsx +1 -1
  37. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/_f_/authentication/sessionTimeout/SessionTimeoutValidator.tsx +12 -18
  38. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/accountSearchService.ts +46 -0
  39. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/query/distinctAccountIndustries.graphql +19 -0
  40. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/query/distinctAccountTypes.graphql +19 -0
  41. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/query/getAccountDetail.graphql +121 -0
  42. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/api/account/query/searchAccounts.graphql +51 -0
  43. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +361 -0
  44. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +305 -0
  45. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/pages/Home.tsx +33 -11
  46. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/src/routes.tsx +3 -3
  47. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/vite.config.ts +1 -1
  48. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppAuthUtils.cls → UIBundleAuthUtils.cls} +1 -1
  49. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppChangePassword.cls → UIBundleChangePassword.cls} +2 -2
  50. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppForgotPassword.cls → UIBundleForgotPassword.cls} +2 -2
  51. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppLogin.cls → UIBundleLogin.cls} +6 -6
  52. package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppRegistration.cls → UIBundleRegistration.cls} +10 -10
  53. package/lib/templates/project/reactexternalapp/_p_/_m_/networks/{reactexternalapp.network → reactexternalapp.network-meta.xml} +2 -2
  54. package/lib/templates/project/reactexternalapp/package.json +2 -2
  55. package/lib/templates/project/reactexternalapp/scripts/graphql-search.sh +4 -4
  56. package/lib/templates/project/reactexternalapp/scripts/setup-cli.mjs +51 -51
  57. package/lib/templates/project/reactexternalapp/scripts/sf-project-setup.mjs +16 -16
  58. package/lib/templates/project/reactinternalapp/AGENT.md +152 -46
  59. package/lib/templates/project/reactinternalapp/CHANGELOG.md +366 -208
  60. package/lib/templates/project/reactinternalapp/README.md +12 -12
  61. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/CHANGELOG.md +1 -1
  62. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/README.md +1 -1
  63. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/eslint.config.js +13 -2
  64. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package.json +4 -4
  65. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/reactinternalapp.uibundle-meta.xml +7 -0
  66. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/scripts/get-graphql-schema.mjs +1 -1
  67. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/_ex_/pages/AccountSearch.tsx +15 -6
  68. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/FilterContext.tsx +13 -3
  69. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/BooleanFilter.tsx +9 -5
  70. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/DateFilter.tsx +15 -8
  71. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/DateRangeFilter.tsx +8 -7
  72. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/FilterFieldWrapper.tsx +33 -0
  73. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/MultiSelectFilter.tsx +4 -5
  74. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/NumericRangeFilter.tsx +118 -40
  75. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SearchFilter.tsx +24 -11
  76. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/SelectFilter.tsx +9 -5
  77. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/components/filters/TextFilter.tsx +29 -12
  78. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useDebouncedCallback.ts +34 -0
  79. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/hooks/useObjectSearchParams.ts +10 -5
  80. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/debounce.ts +4 -1
  81. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/_f_/_os_/utils/filterUtils.ts +24 -1
  82. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/accountSearchService.ts +46 -0
  83. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/query/distinctAccountIndustries.graphql +19 -0
  84. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/query/distinctAccountTypes.graphql +19 -0
  85. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/query/getAccountDetail.graphql +121 -0
  86. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/api/account/query/searchAccounts.graphql +51 -0
  87. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/components/AgentforceConversationClient.tsx +5 -2
  88. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountObjectDetailPage.tsx +361 -0
  89. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/AccountSearch.tsx +305 -0
  90. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/pages/Home.tsx +33 -11
  91. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/routes.tsx +12 -1
  92. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/src/types/conversation.ts +2 -0
  93. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/vite.config.ts +1 -1
  94. package/lib/templates/project/reactinternalapp/package.json +2 -2
  95. package/lib/templates/project/reactinternalapp/scripts/graphql-search.sh +4 -4
  96. package/lib/templates/project/reactinternalapp/scripts/setup-cli.mjs +51 -51
  97. package/lib/templates/project/reactinternalapp/scripts/sf-project-setup.mjs +16 -16
  98. package/lib/templates/{webapplication → uiBundles}/reactbasic/CHANGELOG.md +1 -1
  99. package/lib/templates/{webapplication → uiBundles}/reactbasic/README.md +10 -10
  100. package/lib/templates/{webapplication/reactbasic/_webapplication.webapplication-meta.xml → uiBundles/reactbasic/_uibundle.uibundle-meta.xml} +3 -3
  101. package/lib/templates/{webapplication → uiBundles}/reactbasic/eslint.config.js +13 -2
  102. package/lib/templates/{webapplication → uiBundles}/reactbasic/package.json +3 -3
  103. package/lib/templates/{webapplication → uiBundles}/reactbasic/scripts/get-graphql-schema.mjs +1 -1
  104. package/lib/templates/{webapplication → uiBundles}/reactbasic/vite.config.ts +1 -1
  105. package/lib/templates/uiBundles/webappbasic/README.md +15 -0
  106. package/lib/templates/{webapplication/webappbasic/_webapplication.webapplication-meta.xml → uiBundles/webappbasic/_uibundle.uibundle-meta.xml} +3 -3
  107. package/lib/tsconfig.tsbuildinfo +1 -1
  108. package/lib/utils/constants.d.ts +1 -0
  109. package/lib/utils/constants.js +2 -1
  110. package/lib/utils/constants.js.map +1 -1
  111. package/lib/utils/template-placeholders.d.ts +1 -1
  112. package/lib/utils/template-placeholders.js +11 -4
  113. package/lib/utils/template-placeholders.js.map +1 -1
  114. package/lib/utils/types.d.ts +5 -5
  115. package/lib/utils/types.js +3 -3
  116. package/lib/utils/types.js.map +1 -1
  117. package/lib/utils/{webappTemplateUtils.d.ts → uiBundleTemplateUtils.d.ts} +7 -5
  118. package/lib/utils/{webappTemplateUtils.js → uiBundleTemplateUtils.js} +15 -12
  119. package/lib/utils/uiBundleTemplateUtils.js.map +1 -0
  120. package/package.json +5 -5
  121. package/lib/generators/webApplicationGenerator.js.map +0 -1
  122. package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/reactexternalapp.webapplication-meta.xml +0 -7
  123. package/lib/templates/project/reactexternalapp/_r_/webapp-data.md +0 -353
  124. package/lib/templates/project/reactexternalapp/_r_/webapp-ui.md +0 -16
  125. package/lib/templates/project/reactexternalapp/package-lock.json +0 -9995
  126. package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/reactinternalapp.webapplication-meta.xml +0 -7
  127. package/lib/templates/project/reactinternalapp/_r_/webapp-data.md +0 -353
  128. package/lib/templates/project/reactinternalapp/_r_/webapp-ui.md +0 -16
  129. package/lib/templates/project/reactinternalapp/package-lock.json +0 -9995
  130. package/lib/templates/webapplication/reactbasic/e2e/app.spec.ts +0 -17
  131. package/lib/templates/webapplication/webappbasic/README.md +0 -15
  132. package/lib/utils/webappTemplateUtils.js.map +0 -1
  133. /package/lib/templates/project/reactexternalapp/_p_/_m_/_d_/_s_/{_a1_ → reactexternalapp1}/reactexternalapp1.digitalExperience-meta.xml +0 -0
  134. /package/lib/templates/project/reactexternalapp/_p_/_m_/_d_/_s_/{_a1_ → reactexternalapp1}/sfdc_cms__site/reactexternalapp1/_meta.json +0 -0
  135. /package/lib/templates/project/reactexternalapp/_p_/_m_/_d_/_s_/{_a1_ → reactexternalapp1}/sfdc_cms__site/reactexternalapp1/content.json +0 -0
  136. /package/lib/templates/project/reactexternalapp/_p_/_m_/{digitalExperienceConfigs/reactexternalapp1.digitalExperienceConfig → _dc_/reactexternalapp1.digitalExperienceConfig-meta.xml} +0 -0
  137. /package/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/{webapplication.json → ui-bundle.json} +0 -0
  138. /package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppAuthUtils.cls-meta.xml → UIBundleAuthUtils.cls-meta.xml} +0 -0
  139. /package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppChangePassword.cls-meta.xml → UIBundleChangePassword.cls-meta.xml} +0 -0
  140. /package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppForgotPassword.cls-meta.xml → UIBundleForgotPassword.cls-meta.xml} +0 -0
  141. /package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppLogin.cls-meta.xml → UIBundleLogin.cls-meta.xml} +0 -0
  142. /package/lib/templates/project/reactexternalapp/_p_/_m_/classes/{WebAppRegistration.cls-meta.xml → UIBundleRegistration.cls-meta.xml} +0 -0
  143. /package/lib/templates/project/reactexternalapp/_p_/_m_/sites/{reactexternalapp.site → reactexternalapp.site-meta.xml} +0 -0
  144. /package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/{webapplication.json → ui-bundle.json} +0 -0
  145. /package/lib/templates/{webapplication → uiBundles}/reactbasic/.forceignore +0 -0
  146. /package/lib/templates/{webapplication → uiBundles}/reactbasic/.graphqlrc.yml +0 -0
  147. /package/lib/templates/{webapplication → uiBundles}/reactbasic/.prettierignore +0 -0
  148. /package/lib/templates/{webapplication → uiBundles}/reactbasic/.prettierrc +0 -0
  149. /package/lib/templates/{webapplication → uiBundles}/reactbasic/codegen.yml +0 -0
  150. /package/lib/templates/{webapplication → uiBundles}/reactbasic/components.json +0 -0
  151. /package/lib/templates/{webapplication → uiBundles}/reactbasic/index.html +0 -0
  152. /package/lib/templates/{webapplication → uiBundles}/reactbasic/playwright.config.ts +0 -0
  153. /package/lib/templates/{webapplication → uiBundles}/reactbasic/scripts/rewrite-e2e-assets.mjs +0 -0
  154. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/api/graphqlClient.ts +0 -0
  155. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/app.tsx +0 -0
  156. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/appLayout.tsx +0 -0
  157. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/assets/icons/book.svg +0 -0
  158. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/assets/icons/copy.svg +0 -0
  159. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/assets/icons/rocket.svg +0 -0
  160. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/assets/icons/star.svg +0 -0
  161. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/assets/images/codey-1.png +0 -0
  162. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/assets/images/codey-2.png +0 -0
  163. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/assets/images/codey-3.png +0 -0
  164. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/assets/images/vibe-codey.svg +0 -0
  165. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/alerts/status-alert.tsx +0 -0
  166. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/layouts/card-layout.tsx +0 -0
  167. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/alert.tsx +0 -0
  168. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/badge.tsx +0 -0
  169. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/breadcrumb.tsx +0 -0
  170. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/button.tsx +0 -0
  171. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/calendar.tsx +0 -0
  172. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/card.tsx +0 -0
  173. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/checkbox.tsx +0 -0
  174. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/collapsible.tsx +0 -0
  175. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/datePicker.tsx +0 -0
  176. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/dialog.tsx +0 -0
  177. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/field.tsx +0 -0
  178. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/index.ts +0 -0
  179. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/input.tsx +0 -0
  180. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/label.tsx +0 -0
  181. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/pagination.tsx +0 -0
  182. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/popover.tsx +0 -0
  183. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/select.tsx +0 -0
  184. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/separator.tsx +0 -0
  185. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/skeleton.tsx +0 -0
  186. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/sonner.tsx +0 -0
  187. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/spinner.tsx +0 -0
  188. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/table.tsx +0 -0
  189. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/components/ui/tabs.tsx +0 -0
  190. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/lib/utils.ts +0 -0
  191. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/navigationMenu.tsx +0 -0
  192. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/pages/Home.tsx +0 -0
  193. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/pages/NotFound.tsx +0 -0
  194. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/router-utils.tsx +0 -0
  195. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/routes.tsx +0 -0
  196. /package/lib/templates/{webapplication → uiBundles}/reactbasic/src/styles/global.css +0 -0
  197. /package/lib/templates/{webapplication → uiBundles}/reactbasic/tsconfig.json +0 -0
  198. /package/lib/templates/{webapplication → uiBundles}/reactbasic/tsconfig.node.json +0 -0
  199. /package/lib/templates/{webapplication/reactbasic/webapplication.json → uiBundles/reactbasic/ui-bundle.json} +0 -0
  200. /package/lib/templates/{webapplication → uiBundles}/reactbasic/vite-env.d.ts +0 -0
  201. /package/lib/templates/{webapplication → uiBundles}/reactbasic/vitest-env.d.ts +0 -0
  202. /package/lib/templates/{webapplication → uiBundles}/reactbasic/vitest.config.ts +0 -0
  203. /package/lib/templates/{webapplication → uiBundles}/reactbasic/vitest.setup.ts +0 -0
  204. /package/lib/templates/{webapplication → uiBundles}/webappbasic/src/index.html +0 -0
  205. /package/lib/templates/{webapplication/webappbasic/webapplication.json → uiBundles/webappbasic/ui-bundle.json} +0 -0
@@ -4,41 +4,41 @@ An external React starter template for customer-facing apps on the Salesforce pl
4
4
 
5
5
  ## What's included
6
6
 
7
- | Path | Description |
8
- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
9
- | `force-app/main/default/webapplications/reactexternalapp/` | React web app (source, config, tests) |
10
- | `force-app/main/default/classes/` | Apex classes for authentication — `WebAppAuthUtils`, `WebAppChangePassword`, `WebAppForgotPassword`, `WebAppLogin`, `WebAppRegistration` |
11
- | `force-app/main/default/digitalExperienceConfigs/` | Experience Cloud site configuration |
12
- | `force-app/main/default/digitalExperiences/` | Experience Cloud site definition |
13
- | `force-app/main/default/networks/` | Experience Cloud network |
14
- | `force-app/main/default/sites/` | Salesforce site |
7
+ | Path | Description |
8
+ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `force-app/main/default/uiBundles/reactexternalapp/` | React UI Bundle (source, config, tests) |
10
+ | `force-app/main/default/classes/` | Apex classes for authentication — `UIBundleAuthUtils`, `UIBundleChangePassword`, `UIBundleForgotPassword`, `UIBundleLogin`, `UIBundleRegistration` |
11
+ | `force-app/main/default/digitalExperienceConfigs/` | Experience Cloud site configuration |
12
+ | `force-app/main/default/digitalExperiences/` | Experience Cloud site definition |
13
+ | `force-app/main/default/networks/` | Experience Cloud network |
14
+ | `force-app/main/default/sites/` | Salesforce site |
15
15
 
16
16
  ## Getting started
17
17
 
18
- Navigate to the web app and install dependencies:
18
+ Navigate to the UI Bundle and install dependencies:
19
19
 
20
20
  ```bash
21
- cd force-app/main/default/webapplications/reactexternalapp
21
+ cd force-app/main/default/uiBundles/reactexternalapp
22
22
  npm install
23
23
  npm run dev
24
24
  ```
25
25
 
26
- Opens at http://localhost:5173 by default. For build and test instructions, see the [web app README](force-app/main/default/webapplications/reactexternalapp/README.md).
26
+ Opens at http://localhost:5173 by default. For build and test instructions, see the [UI Bundle README](force-app/main/default/uiBundles/reactexternalapp/README.md).
27
27
 
28
28
  ## Deploy
29
29
 
30
- ### Deploy everything (metadata + Experience Cloud site + web app)
30
+ ### Deploy everything (metadata + Experience Cloud site + UI Bundle)
31
31
 
32
32
  ```bash
33
- cd force-app/main/default/webapplications/reactexternalapp && npm install && npm run build && cd -
33
+ cd force-app/main/default/uiBundles/reactexternalapp && npm install && npm run build && cd -
34
34
  sf project deploy start --source-dir force-app --target-org <alias>
35
35
  ```
36
36
 
37
- ### Deploy the web app only
37
+ ### Deploy the UI Bundle only
38
38
 
39
39
  ```bash
40
- cd force-app/main/default/webapplications/reactexternalapp && npm install && npm run build && cd -
41
- sf project deploy start --source-dir force-app/main/default/webapplications --target-org <alias>
40
+ cd force-app/main/default/uiBundles/reactexternalapp && npm install && npm run build && cd -
41
+ sf project deploy start --source-dir force-app/main/default/ui-bundles --target-org <alias>
42
42
  ```
43
43
 
44
44
  ### Deploy Experience Cloud site only
@@ -7,4 +7,4 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
7
7
 
8
8
  ### Features
9
9
 
10
- - auto bump base react app versions and fix issue with base webapplication json ([#175](https://github.com/salesforce-experience-platform-emu/webapps/issues/175)) ([048b5a8](https://github.com/salesforce-experience-platform-emu/webapps/commit/048b5a8449c899fc923aeebc3c76bc5bf1c5e0d4))
10
+ - auto bump base react app versions and fix issue with base ui-bundle json ([#175](https://github.com/salesforce-experience-platform-emu/webapps/issues/175)) ([048b5a8](https://github.com/salesforce-experience-platform-emu/webapps/commit/048b5a8449c899fc923aeebc3c76bc5bf1c5e0d4))
@@ -24,7 +24,7 @@ Starts the Vite dev server (default: http://localhost:5173).
24
24
  npm run build
25
25
  ```
26
26
 
27
- Writes the production bundle to `dist/` inside the web app folder.
27
+ Writes the production bundle to `dist/` inside the UI Bundle folder.
28
28
 
29
29
  ## Test
30
30
 
@@ -18,7 +18,12 @@ const schemaExists = existsSync(schemaPath);
18
18
  const config = [
19
19
  // Global ignores
20
20
  {
21
- ignores: ['build/**/*', 'dist/**/*', 'coverage/**/*'],
21
+ ignores: [
22
+ 'build/**/*',
23
+ 'dist/**/*',
24
+ 'coverage/**/*',
25
+ 'src/api/graphql-operations-types.ts',
26
+ ],
22
27
  },
23
28
  // Config files and build tools (first to avoid inheritance)
24
29
  {
@@ -89,11 +94,17 @@ const config = [
89
94
  'react/no-unescaped-entities': 'off',
90
95
  '@typescript-eslint/no-unused-vars': [
91
96
  'error',
92
- { argsIgnorePattern: '^_' },
97
+ {
98
+ argsIgnorePattern: '^_',
99
+ varsIgnorePattern: '^_',
100
+ caughtErrorsIgnorePattern: '^_',
101
+ ignoreRestSiblings: true,
102
+ },
93
103
  ],
94
104
  '@typescript-eslint/explicit-function-return-type': 'off',
95
105
  '@typescript-eslint/explicit-module-boundary-types': 'off',
96
106
  '@typescript-eslint/no-explicit-any': 'off',
107
+ 'react-hooks/set-state-in-effect': 'warn',
97
108
  },
98
109
  settings: {
99
110
  react: {
@@ -15,8 +15,8 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.114.0",
19
- "@salesforce/webapp-experimental": "^1.114.0",
18
+ "@salesforce/sdk-data": "file:../../../../../../../../../sdk/sdk-data",
19
+ "@salesforce/ui-bundle": "file:../../../../../../../../../ui-bundle",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
21
  "class-variance-authority": "^0.7.1",
22
22
  "clsx": "^2.1.1",
@@ -43,7 +43,7 @@
43
43
  "@graphql-eslint/eslint-plugin": "^4.1.0",
44
44
  "@graphql-tools/utils": "^11.0.0",
45
45
  "@playwright/test": "^1.49.0",
46
- "@salesforce/vite-plugin-webapp-experimental": "^1.114.0",
46
+ "@salesforce/vite-plugin-ui-bundle": "file:../../../../../../../../../vite-plugin-ui-bundle",
47
47
  "@testing-library/jest-dom": "^6.6.3",
48
48
  "@testing-library/react": "^16.1.0",
49
49
  "@testing-library/user-event": "^14.5.2",
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <UIBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <masterLabel>reactexternalapp</masterLabel>
4
+ <description>A Salesforce UI Bundle.</description>
5
+ <isActive>true</isActive>
6
+ <version>1</version>
7
+ <target>Experience</target>
8
+ </UIBundle>
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { writeFileSync } from 'node:fs';
12
12
  import { resolve } from 'node:path';
13
- import { getOrgInfo } from '@salesforce/webapp-experimental/app';
13
+ import { getOrgInfo } from '@salesforce/ui-bundle/app';
14
14
  import { buildClientSchema, getIntrospectionQuery, printSchema } from 'graphql';
15
15
  import { pruneSchema } from '@graphql-tools/utils';
16
16
 
@@ -62,8 +62,8 @@ const FILTER_CONFIGS: FilterFieldConfig[] = [
62
62
  { field: "Industry", label: "Industry", type: "picklist" },
63
63
  { field: "Type", label: "Type", type: "multipicklist" },
64
64
  { field: "AnnualRevenue", label: "Annual Revenue", type: "numeric" },
65
- { field: "CreatedDate", label: "Created Date", type: "date" },
66
- { field: "LastModifiedDate", label: "Last Modified Date", type: "daterange" },
65
+ { field: "CreatedDate", label: "Created Date", type: "datetime" },
66
+ { field: "LastModifiedDate", label: "Last Modified Date", type: "datetimerange" },
67
67
  ];
68
68
 
69
69
  const ACCOUNT_SORT_CONFIGS: SortFieldConfig<keyof Account_OrderBy>[] = [
@@ -152,7 +152,7 @@ export default function AccountSearch() {
152
152
  </div>
153
153
  </CardHeader>
154
154
  <CollapsibleContent>
155
- <CardContent className="space-y-4 pt-0">
155
+ <CardContent className="space-y-1 pt-0">
156
156
  <SearchFilter
157
157
  field="search"
158
158
  label="Search"
@@ -165,9 +165,18 @@ export default function AccountSearch() {
165
165
  options={industryOptions ?? []}
166
166
  />
167
167
  <MultiSelectFilter field="Type" label="Type" options={typeOptions ?? []} />
168
- <NumericRangeFilter field="AnnualRevenue" label="Annual Revenue" />
169
- <DateFilter field="CreatedDate" label="Created Date" />
170
- <DateRangeFilter field="LastModifiedDate" label="Last Modified Date" />
168
+ <NumericRangeFilter
169
+ field="AnnualRevenue"
170
+ label="Annual Revenue"
171
+ min={0}
172
+ max={1_000_000_000_000}
173
+ />
174
+ <DateFilter field="CreatedDate" label="Created Date" filterType="datetime" />
175
+ <DateRangeFilter
176
+ field="LastModifiedDate"
177
+ label="Last Modified Date"
178
+ filterType="datetimerange"
179
+ />
171
180
  </CardContent>
172
181
  </CollapsibleContent>
173
182
  </Collapsible>
@@ -27,7 +27,14 @@ export function FilterProvider({
27
27
  children,
28
28
  }: FilterProviderProps) {
29
29
  return (
30
- <FilterContext.Provider value={{ filters, onFilterChange, onFilterRemove, onReset }}>
30
+ <FilterContext.Provider
31
+ value={{
32
+ filters,
33
+ onFilterChange,
34
+ onFilterRemove,
35
+ onReset,
36
+ }}
37
+ >
31
38
  {children}
32
39
  </FilterContext.Provider>
33
40
  );
@@ -57,10 +64,13 @@ export function useFilterField(field: string) {
57
64
 
58
65
  export function useFilterPanel() {
59
66
  const { filters, onReset } = useFilterContext();
60
- return { hasActiveFilters: filters.length > 0, resetAll: onReset };
67
+ return {
68
+ hasActiveFilters: filters.length > 0,
69
+ resetAll: onReset,
70
+ };
61
71
  }
62
72
 
63
- interface FilterResetButtonProps extends Omit<React.ComponentProps<typeof Button>, "onClick"> {}
73
+ type FilterResetButtonProps = Omit<React.ComponentProps<typeof Button>, "onClick">;
64
74
 
65
75
  export function FilterResetButton({ children, ...props }: FilterResetButtonProps) {
66
76
  const { hasActiveFilters, resetAll } = useFilterPanel();
@@ -5,9 +5,9 @@ import {
5
5
  SelectTrigger,
6
6
  SelectValue,
7
7
  } from "../../../../components/ui/select";
8
- import { Label } from "../../../../components/ui/label";
9
8
  import { cn } from "../../../../lib/utils";
10
9
  import { useFilterField } from "../FilterContext";
10
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
11
11
  import type { ActiveFilterValue } from "../../utils/filterUtils";
12
12
 
13
13
  const ALL_VALUE = "__all__";
@@ -21,11 +21,15 @@ interface BooleanFilterProps extends Omit<React.ComponentProps<"div">, "onChange
21
21
  export function BooleanFilter({ field, label, helpText, className, ...props }: BooleanFilterProps) {
22
22
  const { value, onChange } = useFilterField(field);
23
23
  return (
24
- <div className={cn("space-y-1.5", className)} {...props}>
25
- <Label htmlFor={`filter-${field}`}>{label}</Label>
24
+ <FilterFieldWrapper
25
+ label={label}
26
+ htmlFor={`filter-${field}`}
27
+ helpText={helpText}
28
+ className={className}
29
+ {...props}
30
+ >
26
31
  <BooleanFilterSelect field={field} label={label} value={value} onChange={onChange} />
27
- {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
28
- </div>
32
+ </FilterFieldWrapper>
29
33
  );
30
34
  }
31
35
 
@@ -1,14 +1,15 @@
1
1
  import { useState } from "react";
2
2
  import { parseISO } from "date-fns";
3
- import { Label } from "../../../../components/ui/label";
4
3
  import {
5
4
  DatePicker,
6
5
  DatePickerTrigger,
7
6
  DatePickerContent,
8
7
  DatePickerCalendar,
9
8
  } from "../../../../components/ui/datePicker";
10
- import { cn } from "../../../../lib/utils";
9
+
11
10
  import { useFilterField } from "../FilterContext";
11
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
12
+ import type { FilterFieldType } from "../../utils/filterUtils";
12
13
  import {
13
14
  Select,
14
15
  SelectContent,
@@ -32,9 +33,17 @@ interface DateFilterProps extends Omit<React.ComponentProps<"div">, "onChange">
32
33
  field: string;
33
34
  label: string;
34
35
  helpText?: string;
36
+ filterType?: FilterFieldType;
35
37
  }
36
38
 
37
- export function DateFilter({ field, label, helpText, className, ...props }: DateFilterProps) {
39
+ export function DateFilter({
40
+ field,
41
+ label,
42
+ helpText,
43
+ filterType = "date",
44
+ className,
45
+ ...props
46
+ }: DateFilterProps) {
38
47
  const { value, onChange } = useFilterField(field);
39
48
 
40
49
  const initialOp: DateOperator = value?.min ? "gt" : "lt";
@@ -63,7 +72,7 @@ export function DateFilter({ field, label, helpText, className, ...props }: Date
63
72
  onChange({
64
73
  field,
65
74
  label,
66
- type: "date",
75
+ type: filterType,
67
76
  value: op,
68
77
  min: f === "min" ? dateStr : undefined,
69
78
  max: f === "max" ? dateStr : undefined,
@@ -71,8 +80,7 @@ export function DateFilter({ field, label, helpText, className, ...props }: Date
71
80
  }
72
81
 
73
82
  return (
74
- <div className={cn("space-y-1.5", className)} {...props}>
75
- <Label>{label}</Label>
83
+ <FilterFieldWrapper label={label} helpText={helpText} className={className} {...props}>
76
84
  <div className="flex gap-2">
77
85
  <Select value={operator} onValueChange={(v) => handleOperatorChange(v as DateOperator)}>
78
86
  <SelectTrigger className="w-full flex-1">
@@ -104,8 +112,7 @@ export function DateFilter({ field, label, helpText, className, ...props }: Date
104
112
  </DatePickerContent>
105
113
  </DatePicker>
106
114
  </div>
107
- {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
108
- </div>
115
+ </FilterFieldWrapper>
109
116
  );
110
117
  }
111
118
 
@@ -1,25 +1,28 @@
1
1
  import type { DateRange } from "react-day-picker";
2
- import { Label } from "../../../../components/ui/label";
3
2
  import {
4
3
  DatePicker,
5
4
  DatePickerRangeTrigger,
6
5
  DatePickerContent,
7
6
  DatePickerCalendar,
8
7
  } from "../../../../components/ui/datePicker";
9
- import { cn } from "../../../../lib/utils";
8
+
10
9
  import { useFilterField } from "../FilterContext";
10
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
11
+ import type { FilterFieldType } from "../../utils/filterUtils";
11
12
  import { toDate, toDateString } from "./DateFilter";
12
13
 
13
14
  interface DateRangeFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
14
15
  field: string;
15
16
  label: string;
16
17
  helpText?: string;
18
+ filterType?: FilterFieldType;
17
19
  }
18
20
 
19
21
  export function DateRangeFilter({
20
22
  field,
21
23
  label,
22
24
  helpText,
25
+ filterType = "daterange",
23
26
  className,
24
27
  ...props
25
28
  }: DateRangeFilterProps) {
@@ -35,7 +38,7 @@ export function DateRangeFilter({
35
38
  onChange({
36
39
  field,
37
40
  label,
38
- type: "daterange",
41
+ type: filterType,
39
42
  min: toDateString(range?.from),
40
43
  max: toDateString(range?.to),
41
44
  });
@@ -43,8 +46,7 @@ export function DateRangeFilter({
43
46
  }
44
47
 
45
48
  return (
46
- <div className={cn("space-y-1.5", className)} {...props}>
47
- <Label>{label}</Label>
49
+ <FilterFieldWrapper label={label} helpText={helpText} className={className} {...props}>
48
50
  <DatePicker>
49
51
  <DatePickerRangeTrigger
50
52
  className="w-full"
@@ -63,7 +65,6 @@ export function DateRangeFilter({
63
65
  />
64
66
  </DatePickerContent>
65
67
  </DatePicker>
66
- {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
67
- </div>
68
+ </FilterFieldWrapper>
68
69
  );
69
70
  }
@@ -0,0 +1,33 @@
1
+ import { Label } from "../../../../components/ui/label";
2
+ import { cn } from "../../../../lib/utils";
3
+
4
+ interface FilterFieldWrapperProps extends React.ComponentProps<"div"> {
5
+ label: string;
6
+ htmlFor?: string;
7
+ helpText?: string;
8
+ error?: string;
9
+ }
10
+
11
+ export function FilterFieldWrapper({
12
+ label,
13
+ htmlFor,
14
+ helpText,
15
+ error,
16
+ className,
17
+ children,
18
+ ...props
19
+ }: FilterFieldWrapperProps) {
20
+ return (
21
+ <div className={cn("space-y-1", className)} {...props}>
22
+ <Label htmlFor={htmlFor}>{label}</Label>
23
+ {children}
24
+ <div className="min-h-4">
25
+ {error ? (
26
+ <p className="text-xs text-destructive">{error}</p>
27
+ ) : (
28
+ helpText && <p className="text-xs text-muted-foreground">{helpText}</p>
29
+ )}
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -4,11 +4,12 @@ import {
4
4
  PopoverTrigger,
5
5
  } from "../../../../components/ui/popover";
6
6
  import { Checkbox } from "../../../../components/ui/checkbox";
7
- import { Label } from "../../../../components/ui/label";
8
7
  import { Button } from "../../../../components/ui/button";
9
8
  import { cn } from "../../../../lib/utils";
9
+ import { Label } from "../../../../components/ui/label";
10
10
  import { ChevronDown } from "lucide-react";
11
11
  import { useFilterField } from "../FilterContext";
12
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
12
13
 
13
14
  interface MultiSelectFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
14
15
  field: string;
@@ -53,8 +54,7 @@ export function MultiSelectFilter({
53
54
  }
54
55
 
55
56
  return (
56
- <div className={cn("space-y-1.5", className)} {...props}>
57
- <Label>{label}</Label>
57
+ <FilterFieldWrapper label={label} helpText={helpText} className={className} {...props}>
58
58
  <Popover>
59
59
  <PopoverTrigger asChild>
60
60
  <Button
@@ -92,7 +92,6 @@ export function MultiSelectFilter({
92
92
  </div>
93
93
  </PopoverContent>
94
94
  </Popover>
95
- {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
96
- </div>
95
+ </FilterFieldWrapper>
97
96
  );
98
97
  }
@@ -1,37 +1,52 @@
1
+ import { useEffect, useState } from "react";
1
2
  import { Input } from "../../../../components/ui/input";
2
- import { Label } from "../../../../components/ui/label";
3
- import { cn } from "../../../../lib/utils";
3
+
4
4
  import { useFilterField } from "../FilterContext";
5
+ import { useDebouncedCallback } from "../../hooks/useDebouncedCallback";
6
+ import { FilterFieldWrapper } from "./FilterFieldWrapper";
5
7
  import type { ActiveFilterValue } from "../../utils/filterUtils";
6
8
 
7
9
  interface NumericRangeFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
8
10
  field: string;
9
11
  label: string;
10
12
  helpText?: string;
13
+ min?: number;
14
+ max?: number;
11
15
  }
12
16
 
13
17
  export function NumericRangeFilter({
14
18
  field,
15
19
  label,
16
20
  helpText,
21
+ min,
22
+ max,
17
23
  className,
18
24
  ...props
19
25
  }: NumericRangeFilterProps) {
20
26
  const { value, onChange } = useFilterField(field);
21
27
  return (
22
- <div className={cn("space-y-1.5", className)} {...props}>
23
- <Label>{label}</Label>
24
- <NumericRangeFilterInputs field={field} label={label} value={value} onChange={onChange} />
25
- {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
26
- </div>
28
+ <NumericRangeFilterInputs
29
+ field={field}
30
+ label={label}
31
+ helpText={helpText}
32
+ value={value}
33
+ onChange={onChange}
34
+ min={min}
35
+ max={max}
36
+ className={className}
37
+ {...props}
38
+ />
27
39
  );
28
40
  }
29
41
 
30
42
  interface NumericRangeFilterInputsProps extends Omit<React.ComponentProps<"div">, "onChange"> {
31
43
  field: string;
32
44
  label: string;
45
+ helpText?: string;
33
46
  value: ActiveFilterValue | undefined;
34
47
  onChange: (value: ActiveFilterValue | undefined) => void;
48
+ min?: number;
49
+ max?: number;
35
50
  minInputProps?: React.ComponentProps<typeof Input>;
36
51
  maxInputProps?: React.ComponentProps<typeof Input>;
37
52
  }
@@ -39,47 +54,110 @@ interface NumericRangeFilterInputsProps extends Omit<React.ComponentProps<"div">
39
54
  export function NumericRangeFilterInputs({
40
55
  field,
41
56
  label,
57
+ helpText,
42
58
  value,
43
59
  onChange,
60
+ min: boundMin,
61
+ max: boundMax,
44
62
  className,
45
- minInputProps,
46
- maxInputProps,
47
63
  ...props
48
64
  }: NumericRangeFilterInputsProps) {
49
- const handleChange = (bound: "min" | "max", v: string) => {
50
- const next = {
51
- field,
52
- label,
53
- type: "numeric" as const,
54
- min: value?.min ?? "",
55
- max: value?.max ?? "",
56
- [bound]: v,
57
- };
58
- if (!next.min && !next.max) {
65
+ const [localMin, setLocalMin] = useState(value?.min ?? "");
66
+ const [localMax, setLocalMax] = useState(value?.max ?? "");
67
+
68
+ const externalMin = value?.min ?? "";
69
+ const externalMax = value?.max ?? "";
70
+ useEffect(() => {
71
+ setLocalMin(externalMin);
72
+ }, [externalMin]);
73
+ useEffect(() => {
74
+ setLocalMax(externalMax);
75
+ }, [externalMax]);
76
+
77
+ const isOutOfBounds = (v: string) => {
78
+ if (v === "") return false;
79
+ const n = Number(v);
80
+ return (boundMin != null && n < boundMin) || (boundMax != null && n > boundMax);
81
+ };
82
+ const minOutOfBounds = isOutOfBounds(localMin);
83
+ const maxOutOfBounds = isOutOfBounds(localMax);
84
+ const isRangeInverted = localMin !== "" && localMax !== "" && Number(localMin) > Number(localMax);
85
+ const hasError = minOutOfBounds || maxOutOfBounds || isRangeInverted;
86
+
87
+ const debouncedOnChange = useDebouncedCallback((min: string, max: string) => {
88
+ if (!min && !max) {
59
89
  onChange(undefined);
60
- } else {
61
- onChange(next);
90
+ return;
62
91
  }
63
- };
92
+ const minNum = min !== "" ? Number(min) : null;
93
+ const maxNum = max !== "" ? Number(max) : null;
94
+ if (minNum != null && maxNum != null && minNum > maxNum) return;
95
+ if (
96
+ minNum != null &&
97
+ ((boundMin != null && minNum < boundMin) || (boundMax != null && minNum > boundMax))
98
+ )
99
+ return;
100
+ if (
101
+ maxNum != null &&
102
+ ((boundMin != null && maxNum < boundMin) || (boundMax != null && maxNum > boundMax))
103
+ )
104
+ return;
105
+ onChange({ field, label, type: "numeric" as const, min, max });
106
+ });
107
+
108
+ const boundsLabel =
109
+ boundMin != null && boundMax != null
110
+ ? `${boundMin}–${boundMax}`
111
+ : boundMin != null
112
+ ? `${boundMin} or more`
113
+ : boundMax != null
114
+ ? `${boundMax} or less`
115
+ : null;
116
+
117
+ const errorMessage = isRangeInverted
118
+ ? "Min must not exceed max"
119
+ : (minOutOfBounds || maxOutOfBounds) && boundsLabel
120
+ ? `Value must be between ${boundsLabel}`
121
+ : undefined;
64
122
 
65
123
  return (
66
- <div className={cn("flex gap-2", className)} {...props}>
67
- <Input
68
- type="number"
69
- placeholder="Min"
70
- value={value?.min ?? ""}
71
- onChange={(e) => handleChange("min", e.target.value)}
72
- aria-label={`${label} minimum`}
73
- {...minInputProps}
74
- />
75
- <Input
76
- type="number"
77
- placeholder="Max"
78
- value={value?.max ?? ""}
79
- onChange={(e) => handleChange("max", e.target.value)}
80
- aria-label={`${label} maximum`}
81
- {...maxInputProps}
82
- />
83
- </div>
124
+ <FilterFieldWrapper
125
+ label={label}
126
+ helpText={helpText}
127
+ error={errorMessage}
128
+ className={className}
129
+ {...props}
130
+ >
131
+ <div className="flex gap-2">
132
+ <Input
133
+ type="number"
134
+ placeholder="Min"
135
+ value={localMin}
136
+ min={boundMin}
137
+ max={boundMax}
138
+ onChange={(e) => {
139
+ const v = e.target.value;
140
+ setLocalMin(v);
141
+ debouncedOnChange(v, localMax);
142
+ }}
143
+ aria-label={`${label} minimum`}
144
+ aria-invalid={hasError || undefined}
145
+ />
146
+ <Input
147
+ type="number"
148
+ placeholder="Max"
149
+ value={localMax}
150
+ min={boundMin}
151
+ max={boundMax}
152
+ onChange={(e) => {
153
+ const v = e.target.value;
154
+ setLocalMax(v);
155
+ debouncedOnChange(localMin, v);
156
+ }}
157
+ aria-label={`${label} maximum`}
158
+ aria-invalid={hasError || undefined}
159
+ />
160
+ </div>
161
+ </FilterFieldWrapper>
84
162
  );
85
163
  }