@salesforce/ui-bundle-template-feature-angular-search 0.0.0-stage → 12.14.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 (339) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +75 -2
  3. package/dist/.forceignore +15 -0
  4. package/dist/.husky/pre-commit +4 -0
  5. package/dist/.prettierignore +11 -0
  6. package/dist/.prettierrc +17 -0
  7. package/dist/CHANGELOG.md +5375 -0
  8. package/dist/README.md +83 -0
  9. package/dist/config/project-scratch-def.json +13 -0
  10. package/dist/eslint.config.js +7 -0
  11. package/dist/force-app/main/default/uiBundles/feature-angular-search/.forceignore +15 -0
  12. package/dist/force-app/main/default/uiBundles/feature-angular-search/.graphqlrc.yml +2 -0
  13. package/dist/force-app/main/default/uiBundles/feature-angular-search/.postcssrc.json +5 -0
  14. package/dist/force-app/main/default/uiBundles/feature-angular-search/.prettierignore +8 -0
  15. package/dist/force-app/main/default/uiBundles/feature-angular-search/.prettierrc +12 -0
  16. package/dist/force-app/main/default/uiBundles/feature-angular-search/CHANGELOG.md +4 -0
  17. package/dist/force-app/main/default/uiBundles/feature-angular-search/README.md +85 -0
  18. package/dist/force-app/main/default/uiBundles/feature-angular-search/angular.json +98 -0
  19. package/dist/force-app/main/default/uiBundles/feature-angular-search/codegen.yml +95 -0
  20. package/dist/force-app/main/default/uiBundles/feature-angular-search/components.json +6 -0
  21. package/dist/force-app/main/default/uiBundles/feature-angular-search/e2e/app.spec.ts +15 -0
  22. package/dist/force-app/main/default/uiBundles/feature-angular-search/esbuild/api-version.mjs +17 -0
  23. package/dist/force-app/main/default/uiBundles/feature-angular-search/esbuild/graphql-codegen.mjs +13 -0
  24. package/dist/force-app/main/default/uiBundles/feature-angular-search/eslint-plugin-angular-graphql.js +148 -0
  25. package/dist/force-app/main/default/uiBundles/feature-angular-search/eslint.config.js +77 -0
  26. package/dist/force-app/main/default/uiBundles/feature-angular-search/feature-angular-search.uibundle-meta.xml +7 -0
  27. package/dist/force-app/main/default/uiBundles/feature-angular-search/middleware/html.mjs +3 -0
  28. package/dist/force-app/main/default/uiBundles/feature-angular-search/middleware/proxy.mjs +12 -0
  29. package/dist/force-app/main/default/uiBundles/feature-angular-search/package.json +69 -0
  30. package/dist/force-app/main/default/uiBundles/feature-angular-search/playwright.config.ts +25 -0
  31. package/dist/force-app/main/default/uiBundles/feature-angular-search/public/favicon.ico +0 -0
  32. package/dist/force-app/main/default/uiBundles/feature-angular-search/scripts/get-graphql-schema.mjs +69 -0
  33. package/dist/force-app/main/default/uiBundles/feature-angular-search/scripts/rewrite-e2e-assets.mjs +30 -0
  34. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/api/data-client.service.spec.ts +184 -0
  35. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/api/data-client.service.ts +74 -0
  36. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.config.ts +20 -0
  37. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.html +1 -0
  38. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.routes.ts +27 -0
  39. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.spec.ts +18 -0
  40. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.ts +15 -0
  41. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/layout/app-layout/app-layout.html +51 -0
  42. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/layout/app-layout/app-layout.spec.ts +63 -0
  43. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/layout/app-layout/app-layout.ts +27 -0
  44. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/alert/alert.html +27 -0
  45. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/alert/alert.ts +41 -0
  46. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/button/button.html +27 -0
  47. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/button/button.ts +59 -0
  48. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/card/card.ts +100 -0
  49. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/collapsible/collapsible.html +38 -0
  50. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/collapsible/collapsible.ts +21 -0
  51. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/date-picker/date-picker.html +20 -0
  52. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/date-picker/date-picker.ts +49 -0
  53. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/date-range-picker/date-range-picker.html +20 -0
  54. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/date-range-picker/date-range-picker.ts +69 -0
  55. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/dialog/dialog.html +20 -0
  56. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/dialog/dialog.ts +40 -0
  57. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/dropdown-menu/dropdown-menu.html +20 -0
  58. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/dropdown-menu/dropdown-menu.ts +100 -0
  59. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/field/field.html +15 -0
  60. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/field/field.ts +28 -0
  61. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/field-appearance.ts +8 -0
  62. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/field-size.ts +8 -0
  63. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/icon/icon.ts +118 -0
  64. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/input/input.html +34 -0
  65. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/input/input.ts +79 -0
  66. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/label/label.ts +21 -0
  67. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/paginator/paginator.html +74 -0
  68. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/paginator/paginator.ts +93 -0
  69. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/paginator/types.ts +13 -0
  70. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/popover/popover.html +13 -0
  71. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/popover/popover.ts +37 -0
  72. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/select/select.html +36 -0
  73. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/select/select.ts +63 -0
  74. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/separator/separator.ts +31 -0
  75. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/skeleton/skeleton.ts +20 -0
  76. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/spinner/spinner.ts +28 -0
  77. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/registry.ts +28 -0
  78. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/sobject/index.ts +19 -0
  79. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/sobject/parse-response.ts +57 -0
  80. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/sobject/query-fragment.ts +169 -0
  81. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/types.ts +44 -0
  82. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/api/search-response.spec.ts +114 -0
  83. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/api/search-response.ts +73 -0
  84. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/api/search.service.spec.ts +124 -0
  85. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/api/search.service.ts +80 -0
  86. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/active-filters/active-filters.html +23 -0
  87. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/active-filters/active-filters.spec.ts +73 -0
  88. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/active-filters/active-filters.ts +77 -0
  89. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/filter-panel/default-filter-panel.html +49 -0
  90. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/filter-panel/default-filter-panel.ts +87 -0
  91. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/filter-panel/filter-box.html +13 -0
  92. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/filter-panel/filter-box.ts +39 -0
  93. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/global-search-box/global-search-box.html +16 -0
  94. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/global-search-box/global-search-box.ts +50 -0
  95. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/default-result-row.html +19 -0
  96. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/default-result-row.ts +91 -0
  97. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/merged-search-results.html +99 -0
  98. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/merged-search-results.ts +145 -0
  99. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/result-item.ts +53 -0
  100. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/search-results.html +10 -0
  101. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/search-results.ts +33 -0
  102. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/source-section.html +84 -0
  103. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/source-section.ts +146 -0
  104. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/scope/scope-selector.html +9 -0
  105. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/scope/scope-selector.ts +45 -0
  106. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search/search.html +55 -0
  107. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search/search.ts +118 -0
  108. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search-bar/search-bar.html +10 -0
  109. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search-bar/search-bar.spec.ts +65 -0
  110. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search-bar/search-bar.ts +72 -0
  111. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/sort-control/sort-control.html +24 -0
  112. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/sort-control/sort-control.spec.ts +67 -0
  113. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/sort-control/sort-control.ts +59 -0
  114. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/constants.ts +17 -0
  115. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/default-search-config.ts +83 -0
  116. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/directives/search-result-template/search-result-template.ts +44 -0
  117. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/boolean-filter.html +8 -0
  118. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/boolean-filter.spec.ts +41 -0
  119. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/boolean-filter.ts +42 -0
  120. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-filter.html +20 -0
  121. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-filter.spec.ts +56 -0
  122. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-filter.ts +78 -0
  123. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-range-filter.html +43 -0
  124. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-range-filter.spec.ts +70 -0
  125. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-range-filter.ts +125 -0
  126. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-field-wrapper.html +16 -0
  127. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-field-wrapper.spec.ts +28 -0
  128. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-field-wrapper.ts +24 -0
  129. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-types.spec.ts +27 -0
  130. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-types.ts +64 -0
  131. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/multi-select-filter.html +11 -0
  132. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/multi-select-filter.spec.ts +42 -0
  133. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/multi-select-filter.ts +42 -0
  134. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/numeric-range-filter.html +32 -0
  135. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/numeric-range-filter.spec.ts +75 -0
  136. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/numeric-range-filter.ts +117 -0
  137. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/select-filter.html +8 -0
  138. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/select-filter.spec.ts +43 -0
  139. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/select-filter.ts +40 -0
  140. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/text-filter.html +9 -0
  141. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/text-filter.spec.ts +54 -0
  142. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/text-filter.ts +70 -0
  143. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/index.ts +55 -0
  144. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/query/build-search-query.spec.ts +157 -0
  145. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/query/build-search-query.ts +95 -0
  146. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/state/filter-mapping.ts +48 -0
  147. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/state/search-state.service.spec.ts +293 -0
  148. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/state/search-state.service.ts +735 -0
  149. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/state/search-state.types.ts +48 -0
  150. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/filter.types.ts +57 -0
  151. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/index.ts +11 -0
  152. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/scope.types.ts +10 -0
  153. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/search-config.types.ts +110 -0
  154. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/search-result.types.ts +68 -0
  155. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/sort.types.ts +15 -0
  156. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/debounce.ts +23 -0
  157. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/field-utils.ts +20 -0
  158. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/filter-utils.spec.ts +335 -0
  159. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/filter-utils.ts +354 -0
  160. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/identifier.ts +19 -0
  161. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/merge-utils.spec.ts +199 -0
  162. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/merge-utils.ts +177 -0
  163. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/scope-utils.ts +19 -0
  164. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/sort-utils.ts +32 -0
  165. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/validate-config.spec.ts +48 -0
  166. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/validate-config.ts +34 -0
  167. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/home/home.html +6 -0
  168. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/home/home.spec.ts +23 -0
  169. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/home/home.ts +13 -0
  170. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/not-found/not-found.html +12 -0
  171. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/not-found/not-found.spec.ts +33 -0
  172. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/not-found/not-found.ts +15 -0
  173. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/search/search-page.html +5 -0
  174. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/search/search-page.ts +19 -0
  175. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/hlm-alert-action.ts +14 -0
  176. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/hlm-alert-description.ts +17 -0
  177. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/hlm-alert-title.ts +17 -0
  178. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/hlm-alert.ts +35 -0
  179. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/index.ts +16 -0
  180. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/button/hlm-button.token.ts +22 -0
  181. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/button/hlm-button.ts +72 -0
  182. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/button/index.ts +6 -0
  183. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/hlm-calendar-multi.ts +195 -0
  184. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/hlm-calendar-range.ts +194 -0
  185. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/hlm-calendar.ts +189 -0
  186. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/hlm-month-year-calendar.ts +119 -0
  187. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/index.ts +16 -0
  188. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-action.ts +12 -0
  189. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-avatar.ts +14 -0
  190. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-content.ts +12 -0
  191. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-description.ts +12 -0
  192. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-footer.ts +12 -0
  193. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-header.ts +15 -0
  194. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-title.ts +12 -0
  195. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card.token.ts +19 -0
  196. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card.ts +22 -0
  197. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/index.ts +28 -0
  198. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/collapsible/hlm-collapsible-content.ts +14 -0
  199. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/collapsible/hlm-collapsible-trigger.ts +9 -0
  200. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/collapsible/hlm-collapsible.ts +15 -0
  201. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/collapsible/index.ts +13 -0
  202. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-multi-input.ts +104 -0
  203. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-anchor.ts +23 -0
  204. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-input.ts +118 -0
  205. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-multi.token.ts +109 -0
  206. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-multi.ts +220 -0
  207. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-trigger.ts +104 -0
  208. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker.token.ts +69 -0
  209. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker.ts +203 -0
  210. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-range-input.ts +120 -0
  211. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-range-picker.token.ts +86 -0
  212. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-range-picker.ts +252 -0
  213. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-month-year-input.ts +105 -0
  214. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-month-year-picker.token.ts +89 -0
  215. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-month-year-picker.ts +196 -0
  216. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/index.ts +38 -0
  217. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-close.ts +9 -0
  218. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-content.ts +74 -0
  219. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-description.ts +17 -0
  220. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-footer.ts +15 -0
  221. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-header.ts +12 -0
  222. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-overlay.ts +27 -0
  223. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-portal.ts +8 -0
  224. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-title.ts +14 -0
  225. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-trigger.ts +14 -0
  226. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog.service.ts +46 -0
  227. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog.ts +24 -0
  228. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/index.ts +35 -0
  229. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-checkbox-indicator.ts +21 -0
  230. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-checkbox.ts +54 -0
  231. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-focus-on-hover.ts +35 -0
  232. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-group.ts +14 -0
  233. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-item-sub-indicator.ts +19 -0
  234. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-item.ts +42 -0
  235. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-label.ts +20 -0
  236. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-radio-indicator.ts +21 -0
  237. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-radio.ts +49 -0
  238. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-separator.ts +12 -0
  239. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-shortcut.ts +15 -0
  240. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-sub-trigger.ts +52 -0
  241. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-sub.ts +53 -0
  242. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-token.ts +26 -0
  243. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-trigger.ts +46 -0
  244. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu.ts +57 -0
  245. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/index.ts +49 -0
  246. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-content.ts +12 -0
  247. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-description.ts +53 -0
  248. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-error.ts +100 -0
  249. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-group.ts +15 -0
  250. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-label.ts +17 -0
  251. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-legend.ts +19 -0
  252. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-separator.ts +24 -0
  253. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-set.ts +15 -0
  254. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-title.ts +15 -0
  255. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field.ts +47 -0
  256. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/index.ts +34 -0
  257. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input/hlm-input.ts +21 -0
  258. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input/index.ts +5 -0
  259. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group-addon.ts +40 -0
  260. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group-button.ts +47 -0
  261. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group-input.ts +17 -0
  262. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group-text.ts +14 -0
  263. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group.ts +18 -0
  264. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/index.ts +19 -0
  265. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/label/hlm-label.ts +17 -0
  266. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/label/index.ts +5 -0
  267. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/label/label.html +11 -0
  268. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-numbered-pagination-query-params.ts +191 -0
  269. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-numbered-pagination.ts +290 -0
  270. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-content.ts +12 -0
  271. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-ellipsis.ts +27 -0
  272. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-item.ts +7 -0
  273. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-link.ts +51 -0
  274. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-next.ts +65 -0
  275. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-previous.ts +65 -0
  276. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination.ts +19 -0
  277. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/index.ts +31 -0
  278. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-content.ts +25 -0
  279. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-description.ts +12 -0
  280. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-header.ts +12 -0
  281. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-portal.ts +8 -0
  282. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-title.ts +12 -0
  283. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-trigger.ts +14 -0
  284. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover.ts +23 -0
  285. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/index.ts +25 -0
  286. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-content.ts +44 -0
  287. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-group.ts +14 -0
  288. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-item.ts +36 -0
  289. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-label.ts +14 -0
  290. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-multiple.ts +37 -0
  291. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-placeholder.ts +17 -0
  292. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-portal.ts +8 -0
  293. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-scroll-down.ts +22 -0
  294. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-scroll-up.ts +22 -0
  295. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-separator.ts +14 -0
  296. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-trigger.ts +59 -0
  297. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-value-template.ts +5 -0
  298. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-value.ts +18 -0
  299. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-values-content.ts +9 -0
  300. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-values.ts +5 -0
  301. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select.ts +37 -0
  302. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/index.ts +52 -0
  303. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/separator/hlm-separator.ts +19 -0
  304. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/separator/index.ts +5 -0
  305. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/skeleton/hlm-skeleton.ts +14 -0
  306. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/skeleton/index.ts +5 -0
  307. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/spinner/hlm-spinner.ts +26 -0
  308. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/spinner/index.ts +5 -0
  309. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/utils/hlm.ts +313 -0
  310. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/utils/index.ts +2 -0
  311. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/utils/provide-spartan-hlm.ts +34 -0
  312. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/utils/async-data.ts +99 -0
  313. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/index.html +13 -0
  314. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/main.ts +5 -0
  315. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/styles.css +139 -0
  316. package/dist/force-app/main/default/uiBundles/feature-angular-search/src/types/sf-globals.d.ts +9 -0
  317. package/dist/force-app/main/default/uiBundles/feature-angular-search/tsconfig.app.json +11 -0
  318. package/dist/force-app/main/default/uiBundles/feature-angular-search/tsconfig.json +36 -0
  319. package/dist/force-app/main/default/uiBundles/feature-angular-search/tsconfig.spec.json +10 -0
  320. package/dist/force-app/main/default/uiBundles/feature-angular-search/ui-bundle.json +7 -0
  321. package/dist/jest.config.js +6 -0
  322. package/dist/package-lock.json +12554 -0
  323. package/dist/package.json +46 -0
  324. package/dist/scripts/apex/hello.apex +10 -0
  325. package/dist/scripts/gitignore-templates.json +4 -0
  326. package/dist/scripts/graphql-search.sh +191 -0
  327. package/dist/scripts/network-senders.mjs +84 -0
  328. package/dist/scripts/org-setup-config-schema.mjs +132 -0
  329. package/dist/scripts/org-setup-dev.mjs +114 -0
  330. package/dist/scripts/org-setup-url.mjs +108 -0
  331. package/dist/scripts/org-setup-utils.mjs +569 -0
  332. package/dist/scripts/org-setup-xml.mjs +283 -0
  333. package/dist/scripts/org-setup.config.json +5 -0
  334. package/dist/scripts/org-setup.mjs +2545 -0
  335. package/dist/scripts/sf-project-setup.mjs +103 -0
  336. package/dist/scripts/soql/account.soql +6 -0
  337. package/dist/scripts/validate-org-setup-config.mjs +38 -0
  338. package/dist/sfdx-project.json +12 -0
  339. package/package.json +43 -4
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ // W-23860670: negative min must clamp to the configured bound.
8
+ import { describe, it, expect } from "vitest";
9
+ import {
10
+ buildFilter,
11
+ buildGlobalQueryClause,
12
+ readSourceParams,
13
+ writeSourceParams,
14
+ } from "./filter-utils";
15
+ import type { ActiveFilterValue, FilterFieldConfig } from "../types/filter.types";
16
+
17
+ describe("buildFilter numeric bounds", () => {
18
+ it("clamps a negative min to the configured bound, and leaves unbounded fields unchanged", () => {
19
+ const configs: FilterFieldConfig[] = [
20
+ { field: "Price", label: "Price", type: "numeric", min: 0 },
21
+ { field: "Quantity", label: "Quantity", type: "numeric" },
22
+ ];
23
+ const filters: ActiveFilterValue[] = [
24
+ { field: "Price", label: "Price", type: "numeric", min: "-500" },
25
+ { field: "Quantity", label: "Quantity", type: "numeric", min: "-500" },
26
+ ];
27
+
28
+ expect(buildFilter(filters, configs)).toEqual({
29
+ and: [{ Price: { gte: 0 } }, { Quantity: { gte: -500 } }],
30
+ });
31
+ });
32
+
33
+ it("clamps the max endpoint into the declared bounds too", () => {
34
+ const configs: FilterFieldConfig[] = [
35
+ { field: "Price", label: "Price", type: "numeric", min: 0 },
36
+ ];
37
+ const filters: ActiveFilterValue[] = [
38
+ { field: "Price", label: "Price", type: "numeric", max: "-500" },
39
+ ];
40
+
41
+ expect(buildFilter(filters, configs)).toEqual({ Price: { lte: 0 } });
42
+ });
43
+ });
44
+
45
+ // W-23935484: LIKE metacharacters in user input must be escaped, not treated as wildcards.
46
+ describe("LIKE pattern escaping", () => {
47
+ const configs: FilterFieldConfig[] = [{ field: "Name", label: "Name", type: "text" }];
48
+
49
+ it("escapes %, _, and \\ in a text filter value", () => {
50
+ const filters: ActiveFilterValue[] = [
51
+ { field: "Name", label: "Name", type: "text", value: "50%_off\\deal" },
52
+ ];
53
+
54
+ expect(buildFilter(filters, configs)).toEqual({
55
+ Name: { like: "%50\\%\\_off\\\\deal%" },
56
+ });
57
+ });
58
+
59
+ it("leaves ordinary characters in a text filter value untouched", () => {
60
+ const filters: ActiveFilterValue[] = [
61
+ { field: "Name", label: "Name", type: "text", value: "widget" },
62
+ ];
63
+
64
+ expect(buildFilter(filters, configs)).toEqual({ Name: { like: "%widget%" } });
65
+ });
66
+
67
+ it("escapes %, _, and \\ in the global query across a single searchable field", () => {
68
+ expect(buildGlobalQueryClause("50%_off\\deal", ["Name"])).toEqual({
69
+ Name: { like: "%50\\%\\_off\\\\deal%" },
70
+ });
71
+ });
72
+
73
+ it("escapes %, _, and \\ in the global query across multiple/nested searchable fields", () => {
74
+ expect(buildGlobalQueryClause("a_b", ["Name", "Owner.Name"])).toEqual({
75
+ or: [{ Name: { like: "%a\\_b%" } }, { Owner: { Name: { like: "%a\\_b%" } } }],
76
+ });
77
+ });
78
+ });
79
+
80
+ // A non-numeric or non-finite URL bound must be dropped, never emitted as NaN/Infinity.
81
+ describe("buildFilter numeric guard against invalid bounds", () => {
82
+ const configs: FilterFieldConfig[] = [{ field: "Amount", label: "Amount", type: "numeric" }];
83
+
84
+ it("drops a non-numeric min when there is no valid max, emitting no clause", () => {
85
+ const filters: ActiveFilterValue[] = [
86
+ { field: "Amount", label: "Amount", type: "numeric", min: "abc" },
87
+ ];
88
+ expect(buildFilter(filters, configs)).toBeUndefined();
89
+ });
90
+
91
+ it("drops a non-numeric min but keeps a valid max", () => {
92
+ const filters: ActiveFilterValue[] = [
93
+ { field: "Amount", label: "Amount", type: "numeric", min: "abc", max: "200" },
94
+ ];
95
+ expect(buildFilter(filters, configs)).toEqual({ Amount: { lte: 200 } });
96
+ });
97
+
98
+ it("keeps both bounds when min and max are valid numbers", () => {
99
+ const filters: ActiveFilterValue[] = [
100
+ { field: "Amount", label: "Amount", type: "numeric", min: "100", max: "200" },
101
+ ];
102
+ expect(buildFilter(filters, configs)).toEqual({ Amount: { gte: 100, lte: 200 } });
103
+ });
104
+
105
+ it("keeps a numeric zero bound (a non-empty string is truthy)", () => {
106
+ const filters: ActiveFilterValue[] = [
107
+ { field: "Amount", label: "Amount", type: "numeric", min: "0" },
108
+ ];
109
+ expect(buildFilter(filters, configs)).toEqual({ Amount: { gte: 0 } });
110
+ });
111
+
112
+ it('drops a non-finite bound like Infinity (Number("1e999"))', () => {
113
+ const filters: ActiveFilterValue[] = [
114
+ { field: "Amount", label: "Amount", type: "numeric", min: "1e999" },
115
+ ];
116
+ expect(buildFilter(filters, configs)).toBeUndefined();
117
+ });
118
+ });
119
+
120
+ describe("buildFilter by type", () => {
121
+ it("builds eq for picklist, boolean and a single multipicklist value, in for several", () => {
122
+ const configs: FilterFieldConfig[] = [];
123
+ const f = (type: ActiveFilterValue["type"], value: string): ActiveFilterValue => ({
124
+ field: "F",
125
+ label: "F",
126
+ type,
127
+ value,
128
+ });
129
+ expect(buildFilter([f("picklist", "A")], configs)).toEqual({ F: { eq: "A" } });
130
+ expect(buildFilter([f("boolean", "true")], configs)).toEqual({ F: { eq: true } });
131
+ expect(buildFilter([f("boolean", "false")], configs)).toEqual({ F: { eq: false } });
132
+ expect(buildFilter([f("multipicklist", "A")], configs)).toEqual({ F: { eq: "A" } });
133
+ expect(buildFilter([f("multipicklist", "A,B")], configs)).toEqual({ F: { in: ["A", "B"] } });
134
+ });
135
+
136
+ it("builds date and datetime ranges", () => {
137
+ const configs: FilterFieldConfig[] = [];
138
+ const range = (type: ActiveFilterValue["type"]): ActiveFilterValue => ({
139
+ field: "D",
140
+ label: "D",
141
+ type,
142
+ min: "2026-01-01",
143
+ max: "2026-01-31",
144
+ });
145
+ expect(buildFilter([range("daterange")], configs)).toEqual({
146
+ and: [{ D: { gte: { value: "2026-01-01" } } }, { D: { lte: { value: "2026-01-31" } } }],
147
+ });
148
+ expect(buildFilter([range("datetimerange")], configs)).toEqual({
149
+ and: [
150
+ { D: { gte: { value: "2026-01-01T00:00:00.000Z" } } },
151
+ { D: { lte: { value: "2026-01-31T23:59:59.999Z" } } },
152
+ ],
153
+ });
154
+ });
155
+ });
156
+
157
+ describe("URL param round-trip", () => {
158
+ const configs: FilterFieldConfig[] = [{ field: "Name", label: "Name", type: "text" }];
159
+
160
+ it("omits sort and page size equal to the defaults", () => {
161
+ const params = new URLSearchParams();
162
+ writeSourceParams(params, "acc", [], { field: "Name", direction: "ASC" }, 10, 0, undefined, {
163
+ sort: { field: "Name", direction: "ASC" },
164
+ pageSize: 10,
165
+ });
166
+ expect(params.toString()).toBe("");
167
+ });
168
+
169
+ it("writes and reads filters, sort and page", () => {
170
+ const params = new URLSearchParams();
171
+ writeSourceParams(
172
+ params,
173
+ "acc",
174
+ [{ field: "Name", label: "Name", type: "text", value: "acme" }],
175
+ { field: "Name", direction: "DESC" },
176
+ 25,
177
+ 2,
178
+ "cursor-abc",
179
+ );
180
+ const read = readSourceParams(params, "acc", configs);
181
+ expect(read.filters).toEqual([
182
+ { field: "Name", label: "Name", type: "text", value: "acme", min: undefined, max: undefined },
183
+ ]);
184
+ expect(read.sort).toEqual({ field: "Name", direction: "DESC" });
185
+ expect(read.pageSize).toBe(25);
186
+ expect(read.pageIndex).toBe(2);
187
+ expect(read.afterCursor).toBe("cursor-abc");
188
+ });
189
+
190
+ it("omits the cursor on page 1 and ignores a stray cursor on read", () => {
191
+ const params = new URLSearchParams();
192
+ writeSourceParams(params, "acc", [], null, undefined, 0, "cursor-abc");
193
+ expect(params.has("s.acc.after")).toBe(false);
194
+
195
+ params.set("s.acc.after", "stray");
196
+ expect(readSourceParams(params, "acc", configs).afterCursor).toBeUndefined();
197
+ });
198
+
199
+ it("keeps cursors independent per source", () => {
200
+ const params = new URLSearchParams();
201
+ writeSourceParams(params, "a", [], null, undefined, 1, "ca");
202
+ writeSourceParams(params, "b", [], null, undefined, 2, "cb");
203
+ expect(readSourceParams(params, "a", configs).afterCursor).toBe("ca");
204
+ expect(readSourceParams(params, "b", configs).afterCursor).toBe("cb");
205
+ });
206
+ });
207
+
208
+ describe("date / datetime comparison operators", () => {
209
+ const filter = (
210
+ type: ActiveFilterValue["type"],
211
+ extra: Partial<ActiveFilterValue>,
212
+ ): ActiveFilterValue => ({ field: "D", label: "D", type, ...extra });
213
+
214
+ it("honours an explicit gt / lt operator carried in value", () => {
215
+ expect(buildFilter([filter("date", { value: "gt", min: "2026-01-05" })], [])).toEqual({
216
+ D: { gt: { value: "2026-01-05" } },
217
+ });
218
+ expect(buildFilter([filter("date", { value: "lt", max: "2026-01-05" })], [])).toEqual({
219
+ D: { lt: { value: "2026-01-05" } },
220
+ });
221
+ });
222
+
223
+ it("applies start/end of day for datetime gt / lt", () => {
224
+ expect(buildFilter([filter("datetime", { value: "gt", min: "2026-01-05" })], [])).toEqual({
225
+ D: { gt: { value: "2026-01-05T00:00:00.000Z" } },
226
+ });
227
+ expect(buildFilter([filter("datetime", { value: "lt", max: "2026-01-05" })], [])).toEqual({
228
+ D: { lt: { value: "2026-01-05T23:59:59.999Z" } },
229
+ });
230
+ });
231
+
232
+ it("falls back to gte when min is set and lte otherwise", () => {
233
+ expect(buildFilter([filter("date", { min: "2026-01-05" })], [])).toEqual({
234
+ D: { gte: { value: "2026-01-05" } },
235
+ });
236
+ expect(buildFilter([filter("datetime", { max: "2026-01-05" })], [])).toEqual({
237
+ D: { lte: { value: "2026-01-05T23:59:59.999Z" } },
238
+ });
239
+ });
240
+
241
+ it("ignores an unrecognised operator in value", () => {
242
+ expect(buildFilter([filter("date", { value: "or", min: "2026-01-05" })], [])).toEqual({
243
+ D: { gte: { value: "2026-01-05" } },
244
+ });
245
+ });
246
+
247
+ it("returns no clause without a bound", () => {
248
+ expect(buildFilter([filter("date", { value: "gt" })], [])).toBeUndefined();
249
+ });
250
+
251
+ it("builds a one-sided daterange from min or max alone", () => {
252
+ expect(buildFilter([filter("daterange", { min: "2026-01-01" })], [])).toEqual({
253
+ D: { gte: { value: "2026-01-01" } },
254
+ });
255
+ expect(buildFilter([filter("daterange", { max: "2026-01-31" })], [])).toEqual({
256
+ D: { lte: { value: "2026-01-31" } },
257
+ });
258
+ });
259
+ });
260
+
261
+ describe("readSourceParams untrusted URL values", () => {
262
+ const cfgs: FilterFieldConfig[] = [
263
+ { field: "Created", label: "Created", type: "date" },
264
+ { field: "Closed", label: "Closed", type: "datetime" },
265
+ {
266
+ field: "Stage",
267
+ label: "Stage",
268
+ type: "picklist",
269
+ options: [{ value: "Open", label: "Open" }],
270
+ },
271
+ {
272
+ field: "Tags",
273
+ label: "Tags",
274
+ type: "multipicklist",
275
+ options: [
276
+ { value: "a", label: "a" },
277
+ { value: "b", label: "b" },
278
+ ],
279
+ },
280
+ { field: "Free", label: "Free", type: "picklist" },
281
+ ];
282
+ const read = (entries: Record<string, string>, options?: { sortBy?: { field: string }[] }) =>
283
+ readSourceParams(new URLSearchParams(entries), "s", cfgs, options as never);
284
+
285
+ it("drops a sort field that is not in sortBy", () => {
286
+ const params = { "s.s.sort": "Secret__c", "s.s.dir": "DESC" };
287
+ expect(read(params, { sortBy: [{ field: "Name" }] }).sort).toBeNull();
288
+ expect(read({ "s.s.sort": "Name" }, { sortBy: [{ field: "Name" }] }).sort).toEqual({
289
+ field: "Name",
290
+ direction: "ASC",
291
+ });
292
+ expect(read(params).sort).toEqual({ field: "Secret__c", direction: "DESC" });
293
+ });
294
+
295
+ it("drops malformed date and datetime strings", () => {
296
+ const r = read({
297
+ "s.s.f.Created.min": "2024-01-02",
298
+ "s.s.f.Created.max": "2024-01-02T00:00:00Z",
299
+ "s.s.f.Closed.min": "not-a-date",
300
+ });
301
+ expect(r.filters).toHaveLength(1);
302
+ expect(r.filters[0]).toMatchObject({ field: "Created", min: "2024-01-02", max: undefined });
303
+ });
304
+
305
+ it("keeps only comparison operators as a date value", () => {
306
+ expect(
307
+ read({ "s.s.f.Created.min": "2024-01-02", "s.s.f.Created": "gt" }).filters[0].value,
308
+ ).toBe("gt");
309
+ expect(
310
+ read({ "s.s.f.Created.min": "2024-01-02", "s.s.f.Created": "evil" }).filters[0].value,
311
+ ).toBeUndefined();
312
+ });
313
+
314
+ it("filters picklist values against configured options", () => {
315
+ expect(read({ "s.s.f.Stage": "Hacked" }).filters).toEqual([]);
316
+ expect(read({ "s.s.f.Stage": "Open" }).filters[0].value).toBe("Open");
317
+ expect(read({ "s.s.f.Tags": "a,zzz,b" }).filters[0].value).toBe("a,b");
318
+ expect(read({ "s.s.f.Tags": "zzz" }).filters).toEqual([]);
319
+ });
320
+
321
+ it("leaves picklist values alone when no options are configured", () => {
322
+ expect(read({ "s.s.f.Free": "anything" }).filters[0].value).toBe("anything");
323
+ });
324
+
325
+ it("bounds the page index", () => {
326
+ expect(read({ "s.s.page": "999999999" }).pageIndex).toBe(1000);
327
+ expect(read({ "s.s.page": "abc" }).pageIndex).toBe(0);
328
+ });
329
+
330
+ it("ignores oversize or control-character cursors", () => {
331
+ expect(read({ "s.s.page": "2", "s.s.after": "x".repeat(513) }).afterCursor).toBeUndefined();
332
+ expect(read({ "s.s.page": "2", "s.s.after": "a\u0000b" }).afterCursor).toBeUndefined();
333
+ expect(read({ "s.s.page": "2", "s.s.after": "ok" }).afterCursor).toBe("ok");
334
+ });
335
+ });
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+
7
+ /**
8
+ * Filter primitives for search.
9
+ *
10
+ * Two responsibilities:
11
+ * 1. Build a GraphQL `where` clause from a list of active filter values.
12
+ * 2. Serialize/deserialize active filters to/from URLSearchParams under a
13
+ * per-source namespace prefix (so multiple sources can coexist on one URL).
14
+ *
15
+ * The URL helpers are namespaced: each source declares its key, and filter
16
+ * params are written as `s.<key>.f.<field>=...`. Sort uses
17
+ * `s.<key>.sort=<field>&s.<key>.dir=ASC|DESC`. Page size and page index use
18
+ * `s.<key>.ps=<size>` and `s.<key>.page=<n>`.
19
+ *
20
+ * The global search term `q=...` is **not** scoped to a source — it's broadcast
21
+ * to every source's `searchableFields` by the query builder.
22
+ */
23
+
24
+ import type { ActiveFilterValue, FilterFieldConfig } from "../types/filter.types";
25
+ import type { SortFieldConfig, SortState } from "../types/sort.types";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // URL serialization (per-source namespace)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const SOURCE_PREFIX = "s.";
32
+ const FILTER_INFIX = ".f.";
33
+ const SORT_SUFFIX = ".sort";
34
+ const DIR_SUFFIX = ".dir";
35
+ const PAGE_SIZE_SUFFIX = ".ps";
36
+ const PAGE_SUFFIX = ".page";
37
+ const CURSOR_SUFFIX = ".after";
38
+
39
+ /** Global search term key (not source-scoped). */
40
+ export const GLOBAL_QUERY_KEY = "q";
41
+
42
+ function filterKey(sourceKey: string, field: string, suffix?: string) {
43
+ return `${SOURCE_PREFIX}${sourceKey}${FILTER_INFIX}${field}${suffix ? `.${suffix}` : ""}`;
44
+ }
45
+
46
+ /**
47
+ * The source's own default sort / page size. When a source's current state
48
+ * matches these, the corresponding URL params are omitted so the query string
49
+ * only ever reflects an *explicit* user choice — resetting filters or returning
50
+ * a source to its defaults yields a clean URL instead of one littered with the
51
+ * defaults. The read path falls back to these same defaults when the params are
52
+ * absent, so the round-trip is lossless.
53
+ */
54
+ export interface SourceParamDefaults {
55
+ sort?: SortState | null;
56
+ pageSize?: number;
57
+ }
58
+
59
+ function sortMatchesDefault(sort: SortState, defaultSort: SortState | null | undefined): boolean {
60
+ return (
61
+ !!defaultSort && sort.field === defaultSort.field && sort.direction === defaultSort.direction
62
+ );
63
+ }
64
+
65
+ export function writeSourceParams(
66
+ params: URLSearchParams,
67
+ sourceKey: string,
68
+ filters: ActiveFilterValue[],
69
+ sort: SortState | null,
70
+ pageSize?: number,
71
+ pageIndex?: number,
72
+ afterCursor?: string,
73
+ defaults?: SourceParamDefaults,
74
+ ): void {
75
+ for (const filter of filters) {
76
+ if (filter.value !== undefined && filter.value !== "") {
77
+ params.set(filterKey(sourceKey, filter.field), filter.value);
78
+ }
79
+ if (filter.min !== undefined && filter.min !== "") {
80
+ params.set(filterKey(sourceKey, filter.field, "min"), filter.min);
81
+ }
82
+ if (filter.max !== undefined && filter.max !== "") {
83
+ params.set(filterKey(sourceKey, filter.field, "max"), filter.max);
84
+ }
85
+ }
86
+ // Only serialize sort when it differs from the source default — otherwise
87
+ // the URL would carry the default the user never chose.
88
+ if (sort && !sortMatchesDefault(sort, defaults?.sort)) {
89
+ params.set(`${SOURCE_PREFIX}${sourceKey}${SORT_SUFFIX}`, sort.field);
90
+ params.set(`${SOURCE_PREFIX}${sourceKey}${DIR_SUFFIX}`, sort.direction);
91
+ }
92
+ if (pageSize !== undefined && pageSize !== defaults?.pageSize) {
93
+ params.set(`${SOURCE_PREFIX}${sourceKey}${PAGE_SIZE_SUFFIX}`, String(pageSize));
94
+ }
95
+ if (pageIndex !== undefined && pageIndex > 0) {
96
+ params.set(`${SOURCE_PREFIX}${sourceKey}${PAGE_SUFFIX}`, String(pageIndex + 1));
97
+ // The cursor is only meaningful past page 1; persisting it lets a deep link
98
+ // or refresh fetch the right page.
99
+ if (afterCursor) params.set(`${SOURCE_PREFIX}${sourceKey}${CURSOR_SUFFIX}`, afterCursor);
100
+ }
101
+ }
102
+
103
+ export interface ReadSourceParamsResult {
104
+ filters: ActiveFilterValue[];
105
+ sort: SortState | null;
106
+ pageSize: number | undefined;
107
+ pageIndex: number;
108
+ /** Cursor for the current page; undefined on page 1. */
109
+ afterCursor: string | undefined;
110
+ }
111
+
112
+ /** Upper bound on a page index read from the URL. */
113
+ export const MAX_PAGE_INDEX = 1000;
114
+ /** Upper bound on the length of an `after` cursor read from the URL. */
115
+ export const MAX_CURSOR_LENGTH = 512;
116
+
117
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
118
+
119
+ /** True when the string has an ASCII control character (C0 range or DEL). */
120
+ function hasControlChars(value: string): boolean {
121
+ for (let i = 0; i < value.length; i++) {
122
+ const code = value.charCodeAt(i);
123
+ if (code <= 0x1f || code === 0x7f) return true;
124
+ }
125
+ return false;
126
+ }
127
+ const DATE_FAMILY = ["date", "daterange", "datetime", "datetimerange"];
128
+ const COMPARISON_OPERATORS = ["gt", "gte", "lt", "lte"];
129
+
130
+ function validDate(v: string | undefined): string | undefined {
131
+ return v !== undefined && DATE_PATTERN.test(v) && !isNaN(Date.parse(v)) ? v : undefined;
132
+ }
133
+
134
+ /** Drops URL-supplied values that the config or type does not allow. */
135
+ function sanitizeFilterValues(
136
+ config: FilterFieldConfig,
137
+ value: string | undefined,
138
+ min: string | undefined,
139
+ max: string | undefined,
140
+ ): { value?: string; min?: string; max?: string } {
141
+ if (DATE_FAMILY.includes(config.type)) {
142
+ return {
143
+ value: value !== undefined && COMPARISON_OPERATORS.includes(value) ? value : undefined,
144
+ min: validDate(min),
145
+ max: validDate(max),
146
+ };
147
+ }
148
+ const known = config.options?.map((o) => o.value);
149
+ if (value && known && known.length > 0) {
150
+ if (config.type === "picklist") {
151
+ return { value: known.includes(value) ? value : undefined, min, max };
152
+ }
153
+ if (config.type === "multipicklist") {
154
+ const kept = value.split(",").filter((v) => known.includes(v));
155
+ return { value: kept.length > 0 ? kept.join(",") : undefined, min, max };
156
+ }
157
+ }
158
+ return { value, min, max };
159
+ }
160
+
161
+ export interface ReadSourceParamsOptions {
162
+ /** Allowed sort fields. A URL sort outside this list is dropped. */
163
+ sortBy?: SortFieldConfig[];
164
+ }
165
+
166
+ /**
167
+ * Reads one source's state from the URL. URL values are untrusted: invalid
168
+ * values are dropped, so a dropped `sort` is `null` and the caller falls back
169
+ * to the source's default sort.
170
+ */
171
+ export function readSourceParams(
172
+ params: URLSearchParams,
173
+ sourceKey: string,
174
+ configs: FilterFieldConfig[],
175
+ options?: ReadSourceParamsOptions,
176
+ ): ReadSourceParamsResult {
177
+ const filters: ActiveFilterValue[] = [];
178
+ for (const config of configs) {
179
+ const { field, label, type } = config;
180
+ const { value, min, max } = sanitizeFilterValues(
181
+ config,
182
+ params.get(filterKey(sourceKey, field)) ?? undefined,
183
+ params.get(filterKey(sourceKey, field, "min")) ?? undefined,
184
+ params.get(filterKey(sourceKey, field, "max")) ?? undefined,
185
+ );
186
+
187
+ const hasValue = value !== undefined && value !== "";
188
+ const hasRange = (min !== undefined && min !== "") || (max !== undefined && max !== "");
189
+ if (hasValue || hasRange) {
190
+ filters.push({ field, label, type, value, min, max });
191
+ }
192
+ }
193
+
194
+ let sort: SortState | null = null;
195
+ const sortField = params.get(`${SOURCE_PREFIX}${sourceKey}${SORT_SUFFIX}`);
196
+ const sortDir = params.get(`${SOURCE_PREFIX}${sourceKey}${DIR_SUFFIX}`);
197
+ if (sortField && (!options?.sortBy || options.sortBy.some((s) => s.field === sortField))) {
198
+ sort = { field: sortField, direction: sortDir === "DESC" ? "DESC" : "ASC" };
199
+ }
200
+
201
+ const pageSizeRaw = params.get(`${SOURCE_PREFIX}${sourceKey}${PAGE_SIZE_SUFFIX}`);
202
+ const pageSize = pageSizeRaw ? parseInt(pageSizeRaw, 10) : undefined;
203
+ const pageRaw = params.get(`${SOURCE_PREFIX}${sourceKey}${PAGE_SUFFIX}`);
204
+ const page = pageRaw ? parseInt(pageRaw, 10) : 1;
205
+ const pageIndex = !isNaN(page) && page > 1 ? Math.min(page - 1, MAX_PAGE_INDEX) : 0;
206
+ const rawCursor = params.get(`${SOURCE_PREFIX}${sourceKey}${CURSOR_SUFFIX}`);
207
+ const afterCursor =
208
+ pageIndex > 0 &&
209
+ rawCursor &&
210
+ rawCursor.length <= MAX_CURSOR_LENGTH &&
211
+ !hasControlChars(rawCursor)
212
+ ? rawCursor
213
+ : undefined;
214
+
215
+ return {
216
+ filters,
217
+ sort,
218
+ pageSize: pageSize && !isNaN(pageSize) ? pageSize : undefined,
219
+ pageIndex,
220
+ afterCursor,
221
+ };
222
+ }
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // GraphQL filter building
226
+ // ---------------------------------------------------------------------------
227
+
228
+ /**
229
+ * Combines active filter values into a GraphQL `where` clause for one source.
230
+ * Returns `undefined` when no constraints are active.
231
+ */
232
+ export function buildFilter(filters: ActiveFilterValue[], configs: FilterFieldConfig[]): unknown {
233
+ const configMap = new Map(configs.map((c) => [c.field, c]));
234
+ const clauses: unknown[] = [];
235
+ for (const filter of filters) {
236
+ const clause = buildSingleFilter(filter, configMap.get(filter.field));
237
+ if (clause) clauses.push(clause);
238
+ }
239
+ if (clauses.length === 0) return undefined;
240
+ if (clauses.length === 1) return clauses[0];
241
+ return { and: clauses };
242
+ }
243
+
244
+ function toStartOfDay(d: string) {
245
+ return `${d}T00:00:00.000Z`;
246
+ }
247
+ function toEndOfDay(d: string) {
248
+ return `${d}T23:59:59.999Z`;
249
+ }
250
+
251
+ /** Escapes LIKE metacharacters (`\`, `%`, `_`) so user input is matched literally. */
252
+ function escapeLikePattern(value: string): string {
253
+ return value.replace(/[\\%_]/g, "\\$&");
254
+ }
255
+
256
+ /**
257
+ * Operator for a single-bound date/datetime filter: an explicit comparison
258
+ * carried in `value` (e.g. "gt" for After, "lt" for Before), else `gte` when a
259
+ * lower bound is set and `lte` otherwise.
260
+ */
261
+ function comparisonOperator(value: string | undefined, min: string | undefined): string {
262
+ if (value && COMPARISON_OPERATORS.includes(value)) return value;
263
+ return min ? "gte" : "lte";
264
+ }
265
+
266
+ function buildSingleFilter(filter: ActiveFilterValue, config?: FilterFieldConfig): unknown {
267
+ const { field, type, value, min, max } = filter;
268
+ switch (type) {
269
+ case "text": {
270
+ if (!value) return null;
271
+ return { [field]: { like: `%${escapeLikePattern(value)}%` } };
272
+ }
273
+ case "picklist": {
274
+ if (!value) return null;
275
+ return { [field]: { eq: value } };
276
+ }
277
+ case "numeric": {
278
+ if (!min && !max) return null;
279
+ const ops: Record<string, number> = {};
280
+ // clamp both endpoints into the declared [min,max] bounds
281
+ const clamp = (n: number) => {
282
+ if (config?.min != null) n = Math.max(n, config.min);
283
+ if (config?.max != null) n = Math.min(n, config.max);
284
+ return n;
285
+ };
286
+ // Drop any endpoint that isn't finite — NaN/Infinity serialize to null on the wire.
287
+ const lo = Number(min);
288
+ if (min && Number.isFinite(lo)) ops["gte"] = clamp(lo);
289
+ const hi = Number(max);
290
+ if (max && Number.isFinite(hi)) ops["lte"] = clamp(hi);
291
+ // If both endpoints were dropped as invalid, emit no clause at all.
292
+ if (Object.keys(ops).length === 0) return null;
293
+ return { [field]: ops };
294
+ }
295
+ case "boolean": {
296
+ if (value === undefined || value === "") return null;
297
+ return { [field]: { eq: value === "true" } };
298
+ }
299
+ case "multipicklist": {
300
+ if (!value) return null;
301
+ const values = value.split(",").filter(Boolean);
302
+ if (values.length === 0) return null;
303
+ if (values.length === 1) return { [field]: { eq: values[0] } };
304
+ return { [field]: { in: values } };
305
+ }
306
+ case "date": {
307
+ if (!min && !max) return null;
308
+ const op = comparisonOperator(value, min);
309
+ const dateStr = min ?? max;
310
+ return { [field]: { [op]: { value: dateStr } } };
311
+ }
312
+ case "daterange": {
313
+ if (!min && !max) return null;
314
+ const clauses: unknown[] = [];
315
+ if (min) clauses.push({ [field]: { gte: { value: min } } });
316
+ if (max) clauses.push({ [field]: { lte: { value: max } } });
317
+ return clauses.length === 1 ? clauses[0] : { and: clauses };
318
+ }
319
+ case "datetime": {
320
+ if (!min && !max) return null;
321
+ const op = comparisonOperator(value, min);
322
+ const dateStr = min ?? max;
323
+ const isoStr = op === "gte" || op === "gt" ? toStartOfDay(dateStr!) : toEndOfDay(dateStr!);
324
+ return { [field]: { [op]: { value: isoStr } } };
325
+ }
326
+ case "datetimerange": {
327
+ if (!min && !max) return null;
328
+ const clauses: unknown[] = [];
329
+ if (min) clauses.push({ [field]: { gte: { value: toStartOfDay(min) } } });
330
+ if (max) clauses.push({ [field]: { lte: { value: toEndOfDay(max) } } });
331
+ return clauses.length === 1 ? clauses[0] : { and: clauses };
332
+ }
333
+ default:
334
+ return null;
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Builds a global-q `or` clause across the source's searchable fields.
340
+ * Returns `undefined` when q is empty or the source has no searchable fields.
341
+ */
342
+ export function buildGlobalQueryClause(q: string, searchableFields: string[]): unknown {
343
+ const trimmed = q.trim();
344
+ if (!trimmed || searchableFields.length === 0) return undefined;
345
+ const clauses = searchableFields.map((path) => {
346
+ const parts = path.split(".");
347
+ let clause: Record<string, unknown> = { like: `%${escapeLikePattern(trimmed)}%` };
348
+ for (let i = parts.length - 1; i >= 0; i--) {
349
+ clause = { [parts[i]]: clause };
350
+ }
351
+ return clause;
352
+ });
353
+ return clauses.length === 1 ? clauses[0] : { or: clauses };
354
+ }